Reduce generated JNI callback IL size#1452
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the generator to reduce emitted IL for JNI callbacks by moving the repeated marshal-boundary wrapper (Begin/EndMarshalMethod + try/catch/finally) into generated __JniMarshalMethodHelper.SafeInvokeAction/SafeInvokeFunc overloads, and splitting each callback into a small n_* thunk plus a __n_* callback body.
Changes:
- Refactor generated callbacks so
n_*becomes a minimal unsafe thunk that forwards to__JniMarshalMethodHelperwith a managed function pointer to__n_*. - Add generated
__JniMarshalMethodHelperoverloads in__NamespaceMapping__.cs(XAJavaInterop1) to centralize marshal-boundary handling. - Update generator expected outputs to reflect the new callback shapes and helper emission.
Show a summary per file
| File | Description |
|---|---|
| tools/generator/SourceWriters/MethodCallback.cs | Splits callback generation into thunk + body and routes through __JniMarshalMethodHelper. |
| tools/generator/SourceWriters/Attributes/DebuggerDisableUserUnhandledExceptionsAttributeAttr.cs | Removes now-unneeded attribute writer (attribute applied on helper methods instead). |
| tools/generator/Java.Interop.Tools.Generator.ObjectModel/NamespaceMapping.cs | Emits __JniMarshalMethodHelper overloads and snapshots marshal delegate set for helper generation. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteUnnestedInterfaceTypes.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteNestedInterfaceTypes.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteNestedInterfaceClass.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteMethodWithCharSequenceArrays.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteInterfaceRedeclaredDefaultMethod.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteInterfaceDefaultPropertyGetterOnly.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteInterfaceDefaultProperty.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteInterfaceDefaultMethod.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteInterface.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteDuplicateInterfaceEventArgs.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteDefaultInterfaceMethodInvoker.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteClass.txt | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteMethodWithCharSequenceArrays.txt | Updates expected NRT callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteInterface.txt | Updates expected NRT callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteClass.txt | Updates expected NRT callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.TestInterfaceImplementation.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.ITestInterface.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.IGenericPropertyInterface.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.IGenericInterface.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.GenericStringPropertyImplementation.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.GenericStringImplementation.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.GenericObjectPropertyImplementation.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Test.ME.GenericImplementation.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Java.Util.IQueue.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Java.Util.IDeque.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/Java.Util.ICollection.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/IInterfaceWithoutNamespace.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/ClassWithoutNamespace.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/TestInterface/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper and updates delegate list expectations. |
| tests/generator-Tests/expected.xaji/Streams/Java.Lang.Throwable.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Streams/Java.IO.OutputStream.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Streams/Java.IO.IOException.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Streams/Java.IO.FilterOutputStream.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Streams/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overload set for Streams scenario. |
| tests/generator-Tests/expected.xaji/ParameterXPath/Xamarin.Test.A.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/ParameterXPath/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for ParameterXPath scenario. |
| tests/generator-Tests/expected.xaji/NormalProperties/Xamarin.Test.SomeObject.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/NormalProperties/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for NormalProperties scenario. |
| tests/generator-Tests/expected.xaji/NormalMethods/Xamarin.Test.C.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/NormalMethods/Xamarin.Test.A.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/NormalMethods/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for NormalMethods scenario. |
| tests/generator-Tests/expected.xaji/NestedTypes/Xamarin.Test.NotificationCompatBase.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/NestedTypes/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for NestedTypes scenario. |
| tests/generator-Tests/expected.xaji/java.lang.Enum/Java.Lang.IComparable.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/java.lang.Enum/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for java.lang.Enum scenario. |
| tests/generator-Tests/expected.xaji/InterfaceMethodsConflict/Xamarin.Test.SomeObject2.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/InterfaceMethodsConflict/Xamarin.Test.SomeObject.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/InterfaceMethodsConflict/Xamarin.Test.II2.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/InterfaceMethodsConflict/Xamarin.Test.II1.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/InterfaceMethodsConflict/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for conflict scenario. |
| tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaCrypto.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/GenericArguments/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for higher-arity GenericArguments scenario. |
| tests/generator-Tests/expected.xaji/CSharpKeywords/Xamarin.Test.CSharpKeywords.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/CSharpKeywords/Java.Lang.Throwable.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/CSharpKeywords/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for CSharpKeywords scenario. |
| tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Text.SpannableStringInternal.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Text.SpannableString.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Text.ISpanned.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Text.ISpannable.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Core_Jar2Xml/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for Core_Jar2Xml scenario. |
| tests/generator-Tests/expected.xaji/Android.Graphics.Color/Xamarin.Test.SomeObject.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Android.Graphics.Color/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for Android.Graphics.Color scenario. |
| tests/generator-Tests/expected.xaji/Adapters/Xamarin.Test.GenericReturnObject.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Adapters/Xamarin.Test.AdapterView.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Adapters/Xamarin.Test.AbsSpinner.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/Adapters/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for Adapters scenario. |
| tests/generator-Tests/expected.xaji/AccessModifiers/Xamarin.Test.TestClass.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/AccessModifiers/Xamarin.Test.PublicClass.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/AccessModifiers/Xamarin.Test.IExtendedInterface.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/AccessModifiers/Xamarin.Test.ExtendPublicClass.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/AccessModifiers/Xamarin.Test.BasePublicClass.cs | Updates expected generated callbacks to use thunk + helper + __n_* body. |
| tests/generator-Tests/expected.xaji/AccessModifiers/NamespaceMapping.cs | Adds expected __JniMarshalMethodHelper overloads for AccessModifiers scenario. |
Copilot's findings
- Files reviewed: 79/79 changed files
- Comments generated: 1
b315f7d to
22db48f
Compare
|
Follow-up from review discussion: I tried making the generated Release comparison:
The local-function shape saves ~43 KB of generated source compared to sibling |
22db48f to
b315f7d
Compare
Split generated JNI callbacks into a tiny native entry thunk plus a body method, and centralize the marshal transition and exception handling in generated SafeInvoke helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
To address the visibility/naming concern without regressing artifact size, I kept the sibling I also measured the static-local-function alternative: it saves a tiny amount of generated source but increases Release runtime |
b315f7d to
9e6df59
Compare
|
/review |
|
✅ Java.Interop PR Reviewer completed successfully! |
There was a problem hiding this comment.
✅ LGTM — solid size optimization with clean design
Summary: This PR centralizes the JNI marshal boundary boilerplate (BeginMarshalMethod / try / catch / OnUserUnhandledException / EndMarshalMethod) into shared SafeInvokeAction/SafeInvokeFunc helpers in JniMarshal, replacing per-callback inline copies with thin n_* forwarding thunks that delegate to __n_* callback bodies via managed function pointers.
What's good
- Impressive size savings: ~660 KB off
Mono.Android.dll(Release), ~1.8 MB off PDB, ~725 KB off generated source - Correct
[DebuggerDisableUserUnhandledExceptions]placement: follows the catch block from the generatedn_*callbacks to the shared helpers - Clean attribute class removal:
DebuggerDisableUserUnhandledExceptionsAttributeAttris fully removed with zero dangling references - Comprehensive test updates: 28 expected output files updated consistently
- Well-documented design rationale: PR description explains the tailcall/arity-cliff investigation and why fnptr-last was chosen
Issues
| Severity | Count |
|---|---|
| 💡 suggestion | 3 |
Notes
- CI: Only
license/clacheck is visible. Build/test CI results were not available at review time — verify CI is green before merging. - The 34 SafeInvoke overloads go up to T0–T15 (16 extra parameters). This should cover virtually all Java methods, but if a binding exceeds this arity the generator will emit an uncompilable call. Worth documenting the limit or adding a generator diagnostic.
- The
Common/expected output files (WriteInterfaceRedeclaredDefaultMethod.txt,WriteDuplicateInterfaceEventArgs.txt) still contain the old inline pattern — verified these are only used by non-XAJavaInterop1 target tests and are not stale.
Generated by Java.Interop PR Reviewer for issue #1452 · ● 9.4M
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
/azp run |
|
No pipelines are associated with this pull request. |
|
✅ Java.Interop PR Reviewer completed successfully! |
There was a problem hiding this comment.
✅ LGTM — Clean, well-designed size optimization
Issue counts: 0 ❌ errors · 0
Summary
This PR cleanly splits JNI callbacks into thin n_* forwarding thunks and __n_* marshal bodies, centralizing the exception-handling boilerplate into shared SafeInvokeAction/SafeInvokeFunc helpers in Java.Interop.JniMarshal. The approach is sound:
- Exception boundary correctness —
[DebuggerDisableUserUnhandledExceptions]correctly follows thecatchblock into the shared helpers. TheBeginMarshalMethod/OnUserUnhandledException/EndMarshalMethodpattern matches the existingManagedPeer.csconvention. - NRT handling —
[return: MaybeNull]onSafeInvokeFuncavoids the null-forgiving operator while correctly handling thedefaultreturn for non-nullableTResulttypes. Good call. - T4 template — Addresses prior review feedback. The template correctly generates all 34 overloads (17 arities × Action/Func) with consistent structure. The
.csprojwiring (TextTemplatingFileGenerator+DependentUpon) follows the existing patterns forJniBuiltinMarshalers.ttand others. - Attribute propagation —
WriteMarshalBodycorrectly re-emitsObsoleteandSupportedOSPlatformattributes on__n_*methods to suppress compiler warnings when the underlying Java method is deprecated. - Accessibility —
__n_*methods are alwaysprivate static, which is correct for both class members (C# default) and interface members (explicitprivaterequired in C# 8+). - Generator cleanup —
DebuggerDisableUserUnhandledExceptionsAttributeAttris cleanly removed with no remaining references. - Test coverage — All expected codegen output files (69 files) are consistently updated to reflect the new pattern.
The PublicAPI surface additions (34 entries in PublicAPI.Unshipped.txt) are complete and match the generated overloads.
CI Status
⏳ The dotnet.java-interop Azure Pipelines build is currently queued/pending on the latest commit (1e9ef1b). The license/cla check passed. Full CI verification is still needed before merge.
Positive callouts
- The measured size reductions are significant: -677 KB runtime Mono.Android.dll, -726 KB generated source. Good use of shared infrastructure.
- The design notes section in the PR description is excellent — documenting the
tail./ arity-cliff investigation and the function-pointer-last rationale helps future maintainers understand why this shape was chosen. - Migrating from hand-written 530-line boilerplate to a 67-line T4 template eliminates copy-paste inconsistency risk.
Generated by Java.Interop PR Reviewer for issue #1452 · ● 9M
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Fixes #1359
Summary
This reduces generated JNI callback IL size by moving the repeated marshal-boundary wrapper code out of every generated callback and into generated helper overloads.
Generated callbacks now have two pieces:
n_*: a small native-entry thunk that forwards JNI arguments to a generated safe invoker.__n_*: the actual callback body that performsGetObject, argument conversion, user call/property access, cleanup, and return marshaling.Java.Interop.JniMarshalnow provides sharedSafeInvokeActionandSafeInvokeFuncoverloads. These helpers centralize:JniEnvironment.BeginMarshalMethod(...)try/catch (Exception)/finallyJniRuntime.OnUserUnhandledException(...)JniEnvironment.EndMarshalMethod(...)The helper shape is intentionally C#-only and uses managed function pointers with the function pointer as the final argument:
Design notes
During prototyping I tested several alternatives:
tail.thunksThe useful findings were:
tail.can help low-arity compiled IL thunks, but has severe arity/ABI cliffs. On arm64, mixed signatures around 8+ args fell back to helper-based tailcalls and became ~4x slower than normal calls.So this PR deliberately avoids explicit
tail.and uses the safer C# helper shape with JNI args first and fnptr last. The safe invokers live inJava.Interop.JniMarshalrather than generated binding output, so each binding assembly only emits the smalln_*/__n_*split and does not get its own copy of the helper methods.DebuggerDisableUserUnhandledExceptions placement
This PR does not remove
[global::System.Diagnostics.DebuggerDisableUserUnhandledExceptions]from the marshal exception boundary. It moves the attribute to the sharedJava.Interop.JniMarshal.SafeInvokeAction/SafeInvokeFunchelpers because those helpers now own thetry/catch (Exception)block and callOnUserUnhandledException(...).The generated
n_*methods are now only forwarding thunks and no longer catch user exceptions. Keeping the attribute on everyn_*thunk would be redundant for debugger behavior and would add back per-callback metadata/IL that this change is trying to remove. The attribute follows the catch block.Mono.Android size measurements
Measured by building Mono.Android from a dotnet/android worktree with this Java.Interop generator patch applied.
The shared-helper version has no generated
__JniMarshalMethodHelpercopies (0matches in generated mcw output). Generated callbacks callJava.Interop.JniMarshal.SafeInvoke*instead (29,967call sites in Mono.Android generated output).Release
Mono.Android.dllMono.Android.dllMono.Android.pdb.cstotalDebug
Mono.Android.dllMono.Android.dllMono.Android.pdb.cstotalCompared with the earlier generated-helper prototype, moving the helper into
Java.Interop.JniMarshalsaves an additional ~11.5 KiB in runtimeMono.Android.dll, ~8.5 KiB in refMono.Android.dll, and ~6.8 KiB of generated mcw source.Measurement notes are saved locally at:
~/.copilot/session-state/fd1f37cb-e759-40da-8b40-c668a63336f6/files/android-issue-1359-shared-helper-measure-summary.txtValidation
dotnet build tools/generator/generator.csproj -v:minimaldotnet test tests/generator-Tests/generator-Tests.csproj -v:minimal