Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
58f0dbd
WIP
simonrozsival Jun 4, 2026
fd4c112
Remove unnecessary suppression
simonrozsival Jun 4, 2026
f2246b3
Move DynamicJniTypeManager to its own separate file
simonrozsival Jun 8, 2026
6d52b54
Update NativeAOT sample
simonrozsival Jun 8, 2026
fa49c9a
Simplify
simonrozsival Jun 8, 2026
17e87cd
Fix dynamic JniTypeManager test regressions
simonrozsival Jun 8, 2026
7f9798e
Use annotations for dynamic type manager tests
simonrozsival Jun 8, 2026
6429366
Keep NativeAOT samples off dynamic type manager
simonrozsival Jun 8, 2026
c18bdc7
Revert extra JniTypeManager test assertions
simonrozsival Jun 8, 2026
a19e758
Move dynamic test annotations to classes
simonrozsival Jun 8, 2026
cd635d8
Remove extra generic invoker test
simonrozsival Jun 8, 2026
6af232f
Use direct type references in NativeAOT samples
simonrozsival Jun 8, 2026
906e8dd
Separate default JRE type manager from AOT path
simonrozsival Jun 8, 2026
ef5d9c8
Handle built-in proxy types in AOT type managers
simonrozsival Jun 9, 2026
a07fda2
Map primitive signatures for AOT constructor lookup
simonrozsival Jun 9, 2026
b1f75c8
Rename dynamic JNI type manager
simonrozsival Jun 9, 2026
5877194
Split JniValueManager and ReflectionJniValueManager
simonrozsival Jun 9, 2026
65e6c60
Undo unnecessary changes
simonrozsival Jun 9, 2026
32f4f4f
Remove unnecessary changes
simonrozsival Jun 9, 2026
8f644e5
Suppress warnigns in tests
simonrozsival Jun 9, 2026
00d3a9d
Merge remote-tracking branch 'origin/main' into dev/simonrozsival/fix…
Copilot Jun 9, 2026
48fbe33
Fix code review issues in ReflectionJniValueManager and ReflectionJni…
Copilot Jun 9, 2026
5dbaa73
Address review comments: EnsureNotDisposed, spacing, Array.Empty, nul…
Copilot Jun 9, 2026
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
3 changes: 1 addition & 2 deletions samples/Hello-NativeAOTFromAndroid/JavaInteropRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,10 @@ static void init (IntPtr jnienv, IntPtr klass)
try {
var options = new JreRuntimeOptions {
EnvironmentPointer = jnienv,
TypeManager = new NativeAotTypeManager (),
JniGlobalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:GREF"),
JniLocalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:LREF"),
};
runtime = options.CreateJreVM ();
runtime = options.CreateJreVM (new NativeAotTypeManager ());
}
catch (Exception e) {
AndroidLog.Print (AndroidLogLevel.Error, "JavaInteropRuntime", $"JavaInteropRuntime.init: error: {e}");
Expand Down
99 changes: 87 additions & 12 deletions samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ partial class NativeAotTypeManager : JniRuntime.JniTypeManager {

internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods;
internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes;
internal const DynamicallyAccessedMemberTypes MethodsConstructors = MethodsAndPrivateNested | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;

Dictionary<string, Type> typeMappings = new () {
["android/app/Activity"] = typeof (Android.App.Activity),
Expand All @@ -25,28 +26,50 @@ public override void RegisterNativeMembers (
Type type,
ReadOnlySpan<char> methods)
{
Console.WriteLine ($"# jonp: RegisterNativeMembers: nativeClass={nativeClass} type=`{type}`");
base.RegisterNativeMembers (nativeClass, type, methods);
if (!methods.IsEmpty)
throw new NotSupportedException ($"Could not register native members for type '{type.FullName}'.");
}

[Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan<char>)")]
public override void RegisterNativeMembers (
JniType nativeClass,
[DynamicallyAccessedMembers (MethodsAndPrivateNested)]
Type type,
string? methods)
{
RegisterNativeMembers (nativeClass, type, methods.AsSpan ());
}

protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpleReference)
{
Console.WriteLine ($"# jonp: GetTypesForSimpleReference: jniSimpleReference=`{jniSimpleReference}`");
if (typeMappings.TryGetValue (jniSimpleReference, out var target)) {
Console.WriteLine ($"# jonp: GetTypesForSimpleReference: jniSimpleReference=`{jniSimpleReference}` -> `{target}`");
var target = GetTypeForSimpleReference (jniSimpleReference);
if (target != null)
yield return target;
}
foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) {
Console.WriteLine ($"# jonp: GetTypesForSimpleReference: jniSimpleReference=`{jniSimpleReference}` -> `{t}`");
yield return t;
}
}

protected override string? GetSimpleReference (Type type)
{
return GetSimpleReferences (type).FirstOrDefault ();
}

[return: DynamicallyAccessedMembers (MethodsConstructors)]
protected override Type? GetTypeForSimpleReference (string jniSimpleReference)
{
return jniSimpleReference switch {
"android/app/Activity" => typeof (Android.App.Activity),
"android/content/Context" => typeof (Android.Content.Context),
"android/content/ContextWrapper" => typeof (Android.Content.ContextWrapper),
"android/os/BaseBundle" => typeof (Android.OS.BaseBundle),
"android/os/Bundle" => typeof (Android.OS.Bundle),
"android/view/ContextThemeWrapper" => typeof (Android.View.ContextThemeWrapper),
"my/MainActivity" => typeof (MainActivity),
_ => null,
};
}
Comment on lines +56 to 68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ Code organization — The type mappings in this GetTypeForSimpleReference switch statement duplicate the typeMappings dictionary on lines 13–21. GetTypeForSimpleReference does forward lookups (string → Type) and CreateSimpleReferencesEnumerator does reverse lookups (Type → string) via the dictionary.

If someone adds a type to one but not the other, forward/reverse resolution will silently diverge. The Hello-NativeAOTFromJNI sample avoids this by encoding both directions directly in code without a dictionary.

Consider either:

  1. Removing the typeMappings dictionary and using if/else type checks in CreateSimpleReferencesEnumerator (matching the Hello-NativeAOTFromJNI pattern), or
  2. Deriving the switch cases from the dictionary to keep a single source of truth.

Rule: Code duplication — single source of truth


protected override IEnumerable<string> GetSimpleReferences (Type type)
{
return base.GetSimpleReferences (type)
.Concat (CreateSimpleReferencesEnumerator (type));
return CreateSimpleReferencesEnumerator (type);
}

IEnumerable<string> CreateSimpleReferencesEnumerator (Type type)
Expand All @@ -58,4 +81,56 @@ IEnumerable<string> CreateSimpleReferencesEnumerator (Type type)
yield return e.Key;
}
}

public override IEnumerable<Type> GetTypes (JniTypeSignature typeSignature)
{
if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null)
return [];
return GetTypesForSimpleReference (typeSignature.SimpleReference);
}

public override IEnumerable<JniRuntime.JniTypeManager.ReflectionConstructibleType> GetReflectionConstructibleTypes (JniTypeSignature typeSignature)
{
if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null)
yield break;
var target = GetTypeForSimpleReference (typeSignature.SimpleReference);
if (target != null)
yield return new JniRuntime.JniTypeManager.ReflectionConstructibleType (target);
}

protected override JniTypeSignature GetTypeSignatureCore (Type type)
{
var simpleReference = GetSimpleReferences (type).FirstOrDefault ();
return simpleReference == null ? default : new JniTypeSignature (simpleReference, 0, false);
}

protected override IEnumerable<JniTypeSignature> GetTypeSignaturesCore (Type type)
{
var signature = GetTypeSignatureCore (type);
if (signature.IsValid)
yield return signature;
}

[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
protected override Type? GetInvokerTypeCore (
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
Type type)
{
return null;
}

protected override IReadOnlyList<string>? GetStaticMethodFallbackTypesCore (string jniSimpleReference)
{
return null;
}

protected override string? GetReplacementTypeCore (string jniSimpleReference)
{
return null;
}

protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature)
{
return null;
}
}
3 changes: 1 addition & 2 deletions samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ static void init (IntPtr jnienv, IntPtr klass)
try {
var options = new JreRuntimeOptions {
EnvironmentPointer = jnienv,
TypeManager = new NativeAotTypeManager (),
};
runtime = options.CreateJreVM ();
runtime = options.CreateJreVM (new NativeAotTypeManager ());
}
catch (Exception e) {
Console.Error.WriteLine ($"JavaInteropRuntime.init: error: {e}");
Expand Down
8 changes: 4 additions & 4 deletions samples/Hello-NativeAOTFromJNI/ManagedType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ namespace Example;
using System;
using Java.Interop;

[System.Runtime.InteropServices.UnmanagedFunctionPointer (System.Runtime.InteropServices.CallingConvention.Winapi)]
delegate IntPtr _JniMarshal_PP_L (IntPtr jnienv, IntPtr n_self);

[JniTypeSignature (JniTypeName)]
class ManagedType : Java.Lang.Object {
internal const string JniTypeName = "example/ManagedType";
Expand All @@ -21,9 +24,6 @@ public Java.Lang.String GetString ()
return new Java.Lang.String ($"Hello from C#, via Java.Interop! Value={value}");
}

[System.Runtime.InteropServices.UnmanagedFunctionPointer (System.Runtime.InteropServices.CallingConvention.Winapi)]
delegate IntPtr _JniMarshal_PP_L (IntPtr jnienv, IntPtr n_self);

static IntPtr n_GetString (IntPtr jnienv, IntPtr n_self)
{
var r_self = new JniObjectReference (n_self);
Expand All @@ -38,7 +38,7 @@ static IntPtr n_GetString (IntPtr jnienv, IntPtr n_self)
}

[JniAddNativeMethodRegistration]
static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
internal static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
{
args.AddRegistrations (new [] {
new JniNativeMethodRegistration ("n_GetString", "()Ljava/lang/String;", new _JniMarshal_PP_L (n_GetString)),
Expand Down
125 changes: 108 additions & 17 deletions samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,128 @@
using Java.Interop;
using System.Diagnostics.CodeAnalysis;

namespace Hello_NativeAOTFromJNI;

class NativeAotTypeManager : JniRuntime.JniTypeManager {

#pragma warning disable IL2026
Dictionary<string, Type> typeMappings = new () {
[Example.ManagedType.JniTypeName] = typeof (Example.ManagedType),
};
#pragma warning restore IL2026

internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods;
internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes;
internal const DynamicallyAccessedMemberTypes MethodsConstructors = MethodsAndPrivateNested | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;

protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpleReference)
{
if (typeMappings.TryGetValue (jniSimpleReference, out var target))
var target = GetTypeForSimpleReference (jniSimpleReference);
if (target != null)
yield return target;
foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference))
yield return t;
}

[return: DynamicallyAccessedMembers (MethodsConstructors)]
protected override Type? GetTypeForSimpleReference (string jniSimpleReference)
{
return jniSimpleReference switch {
Example.ManagedType.JniTypeName => typeof (Example.ManagedType),
"java/lang/Object" => typeof (Java.Lang.Object),
"java/lang/String" => typeof (Java.Lang.String),
_ => null,
};
}

public override IEnumerable<Type> GetTypes (JniTypeSignature typeSignature)
{
if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null)
return [];
return GetTypesForSimpleReference (typeSignature.SimpleReference);
}

public override IEnumerable<JniRuntime.JniTypeManager.ReflectionConstructibleType> GetReflectionConstructibleTypes (JniTypeSignature typeSignature)
{
if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null)
yield break;
var target = GetTypeForSimpleReference (typeSignature.SimpleReference);
if (target != null)
yield return new JniRuntime.JniTypeManager.ReflectionConstructibleType (target);
}

protected override IEnumerable<string> GetSimpleReferences (Type type)
{
return base.GetSimpleReferences (type)
.Concat (CreateSimpleReferencesEnumerator (type));
return CreateSimpleReferencesEnumerator (type);
}

IEnumerable<string> CreateSimpleReferencesEnumerator (Type type)
{
if (typeMappings == null)
yield break;
foreach (var e in typeMappings) {
if (e.Value == type)
yield return e.Key;
if (type == typeof (Example.ManagedType))
yield return Example.ManagedType.JniTypeName;
else if (type == typeof (Java.Lang.Object))
yield return "java/lang/Object";
else if (type == typeof (Java.Lang.String))
yield return "java/lang/String";
}

protected override string? GetSimpleReference (Type type)
{
return GetSimpleReferences (type).FirstOrDefault ();
}

protected override JniTypeSignature GetTypeSignatureCore (Type type)
{
var simpleReference = GetSimpleReference (type);
return simpleReference == null ? default : new JniTypeSignature (simpleReference, 0, false);
}

protected override IEnumerable<JniTypeSignature> GetTypeSignaturesCore (Type type)
{
var signature = GetTypeSignatureCore (type);
if (signature.IsValid)
yield return signature;
}

[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
protected override Type? GetInvokerTypeCore (
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
Type type)
{
return null;
}

protected override IReadOnlyList<string>? GetStaticMethodFallbackTypesCore (string jniSimpleReference)
{
return null;
}

protected override string? GetReplacementTypeCore (string jniSimpleReference)
{
return null;
}

protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature)
{
return null;
}

public override void RegisterNativeMembers (
JniType nativeClass,
[DynamicallyAccessedMembers (MethodsAndPrivateNested)]
Type type,
ReadOnlySpan<char> methods)
{
if (type != typeof (Example.ManagedType)) {
if (!methods.IsEmpty)
throw new NotSupportedException ($"Could not register native members for type '{type.FullName}'.");
return;
}

var registrations = new List<JniNativeMethodRegistration> ();
Example.ManagedType.RegisterNativeMembers (new JniNativeMethodRegistrationArguments (registrations, null));
if (registrations.Count > 0)
nativeClass.RegisterNativeMethods (registrations.ToArray ());
}

[Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan<char>)")]
public override void RegisterNativeMembers (
JniType nativeClass,
[DynamicallyAccessedMembers (MethodsAndPrivateNested)]
Type type,
string? methods)
{
RegisterNativeMembers (nativeClass, type, methods.AsSpan ());
}
}
2 changes: 2 additions & 0 deletions src/Java.Interop/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniRuntime.JniValueManager")]
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniEnvironment.References")]
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniRuntime.JniTypeManager")]
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniRuntime.ReflectionJniTypeManager")]
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniRuntime.ReflectionJniValueManager")]
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniPeerMembers.JniInstanceMethods")]
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniPeerMembers.JniInstanceFields")]
[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniRuntime.CreationOptions")]
Expand Down
3 changes: 1 addition & 2 deletions src/Java.Interop/Java.Interop/JavaPrimitiveArrays.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ namespace Java.Interop {

partial class JniRuntime {

partial class JniTypeManager {

partial class ReflectionJniTypeManager {
readonly struct JniPrimitiveArrayInfo {
public readonly JniTypeSignature JniTypeSignature;
public readonly Type PrimitiveType;
Expand Down
2 changes: 1 addition & 1 deletion src/Java.Interop/Java.Interop/JavaPrimitiveArrays.tt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ namespace Java.Interop {
#>
partial class JniRuntime {

partial class JniTypeManager {
partial class ReflectionJniTypeManager {

readonly struct JniPrimitiveArrayInfo {
public readonly JniTypeSignature JniTypeSignature;
Expand Down
2 changes: 1 addition & 1 deletion src/Java.Interop/Java.Interop/JavaProxyObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ sealed class JavaProxyObject : JavaObject, IEquatable<JavaProxyObject>
static readonly ConditionalWeakTable<object, JavaProxyObject> CachedValues = new ConditionalWeakTable<object, JavaProxyObject> ();

[JniAddNativeMethodRegistrationAttribute]
static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
internal static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
{
args.Registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals)));
args.Registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode)));
Expand Down
2 changes: 1 addition & 1 deletion src/Java.Interop/Java.Interop/JniObjectReferenceOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ partial struct JniObjectReference {
}

partial class JniRuntime {
partial class JniValueManager {
partial class ReflectionJniValueManager {
const JniObjectReferenceOptions DoNotRegisterTarget = (JniObjectReferenceOptions) (1 << 2);
}
}
Expand Down
Loading