Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,22 @@ public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kin
}

/// <summary>
/// Encodes a JNI type as its CLR equivalent matching the MCW-generated <c>n_*</c> callback
/// signatures. JNI boolean (Z) maps to <c>sbyte</c> (matching <c>_JniMarshal_*_B</c> delegates).
/// Use this when constructing member references to <c>n_*</c> methods.
/// Encodes a JNI type as its CLR equivalent for a <c>n_*</c> callback MemberRef, used as the
/// <b>fallback</b> when the real <c>n_*</c> method's signature cannot be read from metadata — which
/// happens for framework callbacks, because the generator scans the compile-time <i>reference</i>
/// assemblies (e.g. <c>Microsoft.Android.Ref</c>) and those strip the <c>private static n_*</c>
/// methods. Framework bindings are always produced by the current (post-#1296) generator, so the
/// blittable encoding is correct for them: JNI boolean → <c>sbyte</c>, JNI char → <c>ushort</c>.
/// When the <c>n_*</c> <i>can</i> be read (NuGet binding implementation assemblies such as AndroidX,
/// which may be pre-#1296 and declare <c>bool</c>/<c>char</c>), the captured signature is emitted via
/// <see cref="EncodeClrTypeName"/> instead of this fallback.
/// </summary>
public static void EncodeClrTypeForCallback (SignatureTypeEncoder encoder, JniParamKind kind)
{
switch (kind) {
case JniParamKind.Boolean: encoder.SByte (); break; // MCW n_* callbacks use sbyte for JNI boolean
case JniParamKind.Boolean: encoder.SByte (); break; // blittable modern n_* encoding (post-#1296)
case JniParamKind.Byte: encoder.SByte (); break;
case JniParamKind.Char: encoder.Char (); break;
case JniParamKind.Char: encoder.UInt16 (); break; // blittable modern n_* encoding (post-#1296)
case JniParamKind.Short: encoder.Int16 (); break;
case JniParamKind.Int: encoder.Int32 (); break;
case JniParamKind.Long: encoder.Int64 (); break;
Expand All @@ -149,6 +155,60 @@ public static void EncodeClrTypeForCallback (SignatureTypeEncoder encoder, JniPa
}
}

/// <summary>
/// True when a JNI kind maps to a CLR type that the MCW <c>n_*</c> callback may declare as either
/// its managed form or a blittable form depending on the generator version that compiled the
/// binding: JNI boolean is <c>bool</c> (pre-#1296) or <c>sbyte</c> (post-#1296), and JNI char is
/// <c>char</c> or <c>ushort</c>. For these, the callback MemberRef is captured from the real
/// <c>n_*</c> method's metadata when available; if it can't be read (framework callbacks live in
/// reference assemblies that strip the private static <c>n_*</c>), it falls back to the blittable
/// encoding via <see cref="EncodeClrTypeForCallback"/>.
/// </summary>
public static bool IsAmbiguousCallbackKind (JniParamKind kind)
=> kind == JniParamKind.Boolean || kind == JniParamKind.Char;

/// <summary>
/// True when a JNI method signature contains a boolean or char parameter/return — the kinds whose
/// <c>n_*</c> callback CLR type is ambiguous across generator versions and must therefore be
/// captured from the real method's metadata (see <see cref="IsAmbiguousCallbackKind"/>).
/// </summary>
public static bool HasAmbiguousCallbackType (string jniSignature)
{
if (IsAmbiguousCallbackKind (ParseReturnType (jniSignature))) {
return true;
}
foreach (var kind in ParseParameterTypes (jniSignature)) {
if (IsAmbiguousCallbackKind (kind)) {
return true;
}
}
return false;
}

/// <summary>
/// Encodes a captured CLR type-name string (e.g. "System.Boolean", "System.SByte", "System.IntPtr")
/// onto a signature, used to emit a callback MemberRef that mirrors the real <c>n_*</c> method.
/// </summary>
public static void EncodeClrTypeName (SignatureTypeEncoder encoder, string clrTypeName)
{
switch (clrTypeName) {
case "System.Boolean": encoder.Boolean (); break;
case "System.SByte": encoder.SByte (); break;
case "System.Byte": encoder.Byte (); break;
case "System.Char": encoder.Char (); break;
case "System.UInt16": encoder.UInt16 (); break;
case "System.Int16": encoder.Int16 (); break;
case "System.Int32": encoder.Int32 (); break;
case "System.UInt32": encoder.UInt32 (); break;
case "System.Int64": encoder.Int64 (); break;
case "System.UInt64": encoder.UInt64 (); break;
case "System.Single": encoder.Single (); break;
case "System.Double": encoder.Double (); break;
case "System.IntPtr": encoder.IntPtr (); break;
default: throw new ArgumentException ($"Cannot encode CLR type '{clrTypeName}' in a native callback MemberRef");
}
}

/// <summary>
/// Validates that a JNI type name has the expected structure (e.g., "com/example/MyClass").
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,20 @@ sealed record UcoMethodData
/// </summary>
public required string JniSignature { get; init; }

/// <summary>
/// CLR type-name strings for the real <c>n_*</c> callback's JNI parameters (excluding the leading
/// jnienv/self IntPtr pair), captured from metadata. When present, the callback MemberRef is
/// emitted to mirror these exactly (correctly distinguishing bool/sbyte and char/ushort). Null
/// when the <c>n_*</c> signature was not resolved.
/// </summary>
public IReadOnlyList<string>? CallbackParameterTypeNames { get; init; }

/// <summary>
/// CLR return type-name string for the real <c>n_*</c> callback, captured from metadata.
/// Null when the <c>n_*</c> signature was not resolved.
/// </summary>
public string? CallbackReturnTypeName { get; init; }

/// <summary>
/// Optional metadata for wrappers that dispatch directly to the managed target
/// instead of forwarding to a generated n_* callback.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy)
AssemblyName = !mm.DeclaringAssemblyName.IsNullOrEmpty () ? mm.DeclaringAssemblyName : peer.AssemblyName,
},
JniSignature = mm.JniSignature,
CallbackParameterTypeNames = mm.NativeCallbackParameterTypeNames,
CallbackReturnTypeName = mm.NativeCallbackReturnTypeName,
ExportMethodDispatch = (mm.IsExport || mm.CallManagedMethodDirectly) ? new ExportMethodDispatchData {
ManagedMethodName = mm.ManagedMethodName,
ParameterTypes = mm.ManagedParameterTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1205,14 +1205,38 @@ MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco, JavaPeerProxyData proxy
JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]);
});

// Callback member reference: uses MCW n_* types (sbyte for boolean)
// Callback member reference: mirror the real MCW n_* method's signature. JNI boolean and char
// are ambiguous (bool/sbyte, char/ushort) across generator versions, so when we could read the
// real n_* method's signature (NuGet binding implementation assemblies, e.g. AndroidX) we use it
// exactly. When we couldn't — framework callbacks, because the generator scans the compile-time
// *reference* assemblies (Microsoft.Android.Ref) which strip the private static n_* methods — we
// fall back to the blittable encoding (sbyte/ushort). That is correct for those callbacks because
// framework bindings are always produced by the current (post-#1296) generator.
var capturedParameterTypeNames = uco.CallbackParameterTypeNames;
var capturedReturnTypeName = uco.CallbackReturnTypeName;
bool hasCapturedCallbackSignature = capturedParameterTypeNames is not null &&
capturedReturnTypeName is not null && capturedParameterTypeNames.Count == jniParams.Count;

Action<BlobEncoder> encodeCallbackSig = sig => sig.MethodSignature ().Parameters (paramCount,
rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrTypeForCallback (rt.Type (), returnKind); },
rt => {
if (isVoid) {
rt.Void ();
} else if (hasCapturedCallbackSignature && capturedReturnTypeName is not null) {
JniSignatureHelper.EncodeClrTypeName (rt.Type (), capturedReturnTypeName);
} else {
JniSignatureHelper.EncodeClrTypeForCallback (rt.Type (), returnKind);
}
},
p => {
p.AddParameter ().Type ().IntPtr ();
p.AddParameter ().Type ().IntPtr ();
for (int j = 0; j < jniParams.Count; j++)
JniSignatureHelper.EncodeClrTypeForCallback (p.AddParameter ().Type (), jniParams [j]);
for (int j = 0; j < jniParams.Count; j++) {
if (hasCapturedCallbackSignature && capturedParameterTypeNames is not null) {
JniSignatureHelper.EncodeClrTypeName (p.AddParameter ().Type (), capturedParameterTypeNames [j]);
} else {
JniSignatureHelper.EncodeClrTypeForCallback (p.AddParameter ().Type (), jniParams [j]);
}
}
});

var callbackTypeHandle = _pe.ResolveTypeRef (uco.CallbackType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ public sealed record MarshalMethodInfo
/// </summary>
public required string NativeCallbackName { get; init; }

/// <summary>
/// CLR type-name strings (e.g. "System.Boolean", "System.SByte", "System.IntPtr") for the
/// real native <c>n_*</c> callback's JNI parameters, in order, excluding the leading
/// <c>jnienv</c>/<c>native__this</c> IntPtr pair. Captured from the actual <c>n_*</c> method's
/// metadata signature so the emitted callback MemberRef matches it exactly — importantly, JNI
/// boolean is <c>bool</c> or <c>sbyte</c> and JNI char is <c>char</c> or <c>ushort</c> depending
/// on which generator version compiled the binding (see java-interop #1296). Null when the
/// <c>n_*</c> method's signature could not be resolved.
/// </summary>
internal IReadOnlyList<string>? NativeCallbackParameterTypeNames { get; init; }

/// <summary>
/// CLR return type-name string for the real native <c>n_*</c> callback, captured from metadata.
/// Null when the <c>n_*</c> method's signature could not be resolved.
/// </summary>
internal string? NativeCallbackReturnTypeName { get; init; }

/// <summary>
/// True if this is a constructor registration.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,103 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan
return false;
}

/// <summary>
/// Resolves the type that declares the native <c>n_*</c> callback. When the [Register] connector
/// names a declaring type (e.g. an <c>*Invoker</c> in another assembly) that type is used;
/// otherwise the callback lives on the scanned method's own declaring type.
/// </summary>
bool TryResolveNativeCallbackType (MethodDefinition methodDef, AssemblyIndex index,
string declaringTypeName, string declaringAssemblyName,
[NotNullWhen (true)] out AssemblyIndex? callbackIndex, out TypeDefinitionHandle callbackTypeHandle)
{
if (!declaringTypeName.IsNullOrEmpty ()) {
if (!declaringAssemblyName.IsNullOrEmpty () &&
TryResolveType (declaringTypeName, declaringAssemblyName, out callbackTypeHandle, out callbackIndex)) {
return true;
}
// Type-only connector (no assembly), or the named assembly wasn't indexed:
// search every indexed assembly for the type by full name.
foreach (var candidate in assemblyCache.Values) {
if (candidate.TypesByFullName.TryGetValue (declaringTypeName, out callbackTypeHandle)) {
callbackIndex = candidate;
return true;
}
}
callbackIndex = null;
callbackTypeHandle = default;
return false;
}

callbackIndex = index;
callbackTypeHandle = methodDef.GetDeclaringType ();
return true;
}

/// <summary>
/// Reads the real native <c>n_*</c> callback method's metadata signature so the emitter can
/// mirror it exactly. The <c>n_*</c> signature is
/// <c>(IntPtr jnienv, IntPtr native__this, &lt;native params...&gt;)</c>; the leading IntPtr pair
/// is dropped and the remaining parameter type names (plus the return type name) are returned.
/// Distinguishing e.g. <c>System.Boolean</c> from <c>System.SByte</c> here is what lets the
/// callback MemberRef bind against bindings compiled by either the pre- or post-#1296 generator.
/// </summary>
static bool TryReadNativeCallbackSignature (AssemblyIndex callbackIndex, TypeDefinitionHandle callbackTypeHandle,
string nativeCallbackName, int jniParameterCount,
[NotNullWhen (true)] out IReadOnlyList<string>? parameterTypeNames, [NotNullWhen (true)] out string? returnTypeName)
{
parameterTypeNames = null;
returnTypeName = null;

var reader = callbackIndex.Reader;
var typeDef = reader.GetTypeDefinition (callbackTypeHandle);
foreach (var methodHandle in typeDef.GetMethods ()) {
var methodDef = reader.GetMethodDefinition (methodHandle);
if ((methodDef.Attributes & MethodAttributes.Static) == 0) {
continue;
}
if (reader.GetString (methodDef.Name) != nativeCallbackName) {
continue;
}

var signature = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null);
// n_* callbacks take (IntPtr jnienv, IntPtr native__this) before the native parameters.
// Verify both the arity and that leading pair so a same-named static method with a
// coincidentally-matching arity can never feed the wrong native param types into the ref.
if (signature.ParameterTypes.Length != jniParameterCount + 2) {
Comment thread
simonrozsival marked this conversation as resolved.
continue;
}
if (signature.ParameterTypes [0] != "System.IntPtr" || signature.ParameterTypes [1] != "System.IntPtr") {
continue;
}

var names = new string [jniParameterCount];
for (int i = 0; i < jniParameterCount; i++) {
names [i] = signature.ParameterTypes [i + 2];
}
parameterTypeNames = names;
returnTypeName = signature.ReturnType;
return true;
}

return false;
}

/// <summary>
/// Captures the real n_* callback signature for a callback declared on <paramref name="declaringType"/>
/// (used by the base-hierarchy [Register] paths, where the callback always lives on a named base type).
/// </summary>
(IReadOnlyList<string>? ParameterTypeNames, string? ReturnTypeName) CaptureNativeCallbackSignature (
TypeRefData declaringType, string nativeCallbackName, string jniSignature)
{
if (TryResolveType (declaringType.ManagedTypeName, declaringType.AssemblyName, out var handle, out var index)) {
int jniParameterCount = JniSignatureHelper.ParseParameterTypes (jniSignature).Count;
if (TryReadNativeCallbackSignature (index, handle, nativeCallbackName, jniParameterCount, out var parameterTypeNames, out var returnTypeName)) {
return (parameterTypeNames, returnTypeName);
}
}
return (null, null);
}

/// <summary>
/// Looks up the [Register] JNI name for a type identified by name + assembly.
/// </summary>
Expand Down Expand Up @@ -1285,12 +1382,18 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con

var registerInfo = result.Value.Info;
bool isConstructor = registerInfo.JniName == "<init>" || registerInfo.JniName == ".ctor";
string nativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor);
var (callbackParameterTypeNames, callbackReturnTypeName) = !isConstructor && JniSignatureHelper.HasAmbiguousCallbackType (registerInfo.Signature)
? CaptureNativeCallbackSignature (result.Value.DeclaringType, nativeCallbackName, registerInfo.Signature)
: (null, null);
return new MarshalMethodInfo {
JniName = registerInfo.JniName,
JniSignature = registerInfo.Signature,
Connector = registerInfo.Connector,
ManagedMethodName = methodName,
NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor),
NativeCallbackName = nativeCallbackName,
NativeCallbackParameterTypeNames = callbackParameterTypeNames,
NativeCallbackReturnTypeName = callbackReturnTypeName,
IsConstructor = isConstructor,
DeclaringTypeName = result.Value.DeclaringType.ManagedTypeName,
DeclaringAssemblyName = result.Value.DeclaringType.AssemblyName,
Expand Down Expand Up @@ -1331,12 +1434,18 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con
// Check if the base property has [Register]
var propRegister = TryGetPropertyRegisterInfo (basePropDef, baseIndex);
if (propRegister is not null && propRegister.Signature is not null) {
string nativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false);
var (callbackParameterTypeNames, callbackReturnTypeName) = JniSignatureHelper.HasAmbiguousCallbackType (propRegister.Signature)
? CaptureNativeCallbackSignature (baseTypeRef, nativeCallbackName, propRegister.Signature)
: (null, null);
return new MarshalMethodInfo {
JniName = propRegister.JniName,
JniSignature = propRegister.Signature,
Connector = propRegister.Connector,
ManagedMethodName = getterName,
NativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false),
NativeCallbackName = nativeCallbackName,
NativeCallbackParameterTypeNames = callbackParameterTypeNames,
NativeCallbackReturnTypeName = callbackReturnTypeName,
IsConstructor = false,
DeclaringTypeName = baseTypeRef.ManagedTypeName,
DeclaringAssemblyName = baseTypeRef.AssemblyName,
Expand Down Expand Up @@ -1406,14 +1515,34 @@ void AddMarshalMethod (List<MarshalMethodInfo> methods, RegisterInfo registerInf
}
}

string nativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor);

// For methods that forward to a generated static n_* callback, capture the real n_* signature
// so the emitted MemberRef mirrors it exactly. This is essential for JNI boolean/char, which
// older bindings declare as bool/char and post-#1296 bindings declare as sbyte/ushort. We
// capture whenever the signature has an ambiguous (boolean/char) type, regardless of whether
// this method currently dispatches directly — a caller may re-target it to n_* forwarding.
IReadOnlyList<string>? nativeCallbackParameterTypeNames = null;
string? nativeCallbackReturnTypeName = null;
if (!isConstructor && !isExport && JniSignatureHelper.HasAmbiguousCallbackType (jniSignature) &&
TryResolveNativeCallbackType (methodDef, index, declaringTypeName, declaringAssemblyName, out var callbackIndex, out var callbackTypeHandle)) {
int jniParameterCount = JniSignatureHelper.ParseParameterTypes (jniSignature).Count;
if (TryReadNativeCallbackSignature (callbackIndex, callbackTypeHandle, nativeCallbackName, jniParameterCount, out var capturedParams, out var capturedReturn)) {
nativeCallbackParameterTypeNames = capturedParams;
nativeCallbackReturnTypeName = capturedReturn;
}
}

methods.Add (new MarshalMethodInfo {
JniName = registerInfo.JniName,
JniSignature = jniSignature,
Connector = registerInfo.Connector,
ManagedMethodName = managedName,
DeclaringTypeName = declaringTypeName,
DeclaringAssemblyName = declaringAssemblyName,
NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor),
NativeCallbackName = nativeCallbackName,
NativeCallbackParameterTypeNames = nativeCallbackParameterTypeNames,
NativeCallbackReturnTypeName = nativeCallbackReturnTypeName,
ManagedParameterTypes = managedParameterTypes,
ManagedParameterExportKinds = parameterKinds,
ManagedReturnType = callManagedMethodDirectly ? EnrichTypeRefWithEnumInfo (managedTypeSig.ReturnType) : new TypeRefData {
Expand Down
Loading