Skip to content

Reduce generated JNI callback IL size#1452

Open
simonrozsival wants to merge 1 commit into
mainfrom
copilot/issue-1359-investigation
Open

Reduce generated JNI callback IL size#1452
simonrozsival wants to merge 1 commit into
mainfrom
copilot/issue-1359-investigation

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 9, 2026

Copy link
Copy Markdown
Member

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 performs GetObject, argument conversion, user call/property access, cleanup, and return marshaling.

Java.Interop.JniMarshal now provides shared SafeInvokeAction and SafeInvokeFunc overloads. These helpers centralize:

  • JniEnvironment.BeginMarshalMethod(...)
  • try / catch (Exception) / finally
  • JniRuntime.OnUserUnhandledException(...)
  • JniEnvironment.EndMarshalMethod(...)

The helper shape is intentionally C#-only and uses managed function pointers with the function pointer as the final argument:

Java.Interop.JniMarshal.SafeInvokeAction (jnienv, native__this, arg0, arg1, &__n_Foo);
Java.Interop.JniMarshal.SafeInvokeFunc (jnienv, native__this, arg0, &__n_Bar);

Design notes

During prototyping I tested several alternatives:

  • explicit IL tail. thunks
  • C# forwarding patterns that might encourage tailcalls
  • NativeAOT output for C# forwarding patterns
  • function pointer first vs. function pointer last

The useful findings were:

  • C# and NativeAOT do not reliably emit tailcalls for these forwarding patterns.
  • Explicit 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.
  • Function-pointer-last preserves JNI argument register/stack placement better than function-pointer-first.

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 in Java.Interop.JniMarshal rather than generated binding output, so each binding assembly only emits the small n_*/__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 shared Java.Interop.JniMarshal.SafeInvokeAction / SafeInvokeFunc helpers because those helpers now own the try / catch (Exception) block and call OnUserUnhandledException(...).

The generated n_* methods are now only forwarding thunks and no longer catch user exceptions. Keeping the attribute on every n_* 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 __JniMarshalMethodHelper copies (0 matches in generated mcw output). Generated callbacks call Java.Interop.JniMarshal.SafeInvoke* instead (29,967 call sites in Mono.Android generated output).

Release

Artifact Baseline Patched Delta
Runtime Mono.Android.dll 42,798,592 42,121,728 -676,864 bytes
Ref Mono.Android.dll 19,295,744 19,339,776 +44,032 bytes
Mono.Android.pdb 32,553,308 30,690,252 -1,863,056 bytes
Generated mcw .cs total 29,234,484 28,508,893 -725,591 bytes

Debug

Artifact Baseline Patched Delta
Runtime Mono.Android.dll 46,168,064 45,507,584 -660,480 bytes
Ref Mono.Android.dll 19,294,720 19,339,264 +44,544 bytes
Mono.Android.pdb 38,057,736 35,815,552 -2,242,184 bytes
Generated mcw .cs total 29,234,484 28,508,893 -725,591 bytes

Compared with the earlier generated-helper prototype, moving the helper into Java.Interop.JniMarshal saves an additional ~11.5 KiB in runtime Mono.Android.dll, ~8.5 KiB in ref Mono.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.txt

Validation

  • dotnet build tools/generator/generator.csproj -v:minimal
  • dotnet test tests/generator-Tests/generator-Tests.csproj -v:minimal
    • 455 passed, 0 failed

Copilot AI review requested due to automatic review settings June 9, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 __JniMarshalMethodHelper with a managed function pointer to __n_*.
  • Add generated __JniMarshalMethodHelper overloads 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

Comment thread tools/generator/SourceWriters/MethodCallback.cs Outdated
@simonrozsival simonrozsival force-pushed the copilot/issue-1359-investigation branch 2 times, most recently from b315f7d to 22db48f Compare June 9, 2026 14:22
@simonrozsival

Copy link
Copy Markdown
Member Author

Follow-up from review discussion: I tried making the generated __n_* body a static local function inside n_* to avoid source-level naming concerns. It compiles and tests pass, but it significantly worsens Mono.Android artifact sizes.

Release comparison:

Shape Runtime Mono.Android.dll Ref Mono.Android.dll PDB generated mcw .cs
Baseline 42,798,592 19,295,744 32,553,308 29,234,484
Shared helper + sibling __n_* 42,121,728 19,339,776 30,690,252 28,508,893
Shared helper + local static __n_* 44,582,400 23,309,824 30,711,020 28,465,999

The local-function shape saves ~43 KB of generated source compared to sibling __n_*, but increases runtime Mono.Android.dll by ~2.46 MB and ref Mono.Android.dll by ~3.97 MB compared to the sibling shape. I reverted the PR branch back to the sibling __n_* shape because artifact size is the primary goal here.

@simonrozsival simonrozsival force-pushed the copilot/issue-1359-investigation branch from 22db48f to b315f7d Compare June 9, 2026 14:37
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>
@simonrozsival

Copy link
Copy Markdown
Member Author

To address the visibility/naming concern without regressing artifact size, I kept the sibling __n_* body methods but made them explicitly private static. This preserves the smaller assembly/ref sizes from the sibling-method shape while making the intended visibility clear.

I also measured the static-local-function alternative: it saves a tiny amount of generated source but increases Release runtime Mono.Android.dll by ~2.46 MB and ref Mono.Android.dll by ~3.97 MB compared to sibling __n_*, so I did not keep that approach.

@simonrozsival simonrozsival force-pushed the copilot/issue-1359-investigation branch from b315f7d to 9e6df59 Compare June 9, 2026 14:41
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, thanks! label Jun 9, 2026
@jonathanpeppers

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ 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 generated n_* callbacks to the shared helpers
  • Clean attribute class removal: DebuggerDisableUserUnhandledExceptionsAttributeAttr is 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/cla check 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

Comment on lines +59 to +68
var call = $"global::Java.Interop.JniMarshal.{(method.IsVoid ? "SafeInvokeAction" : "SafeInvokeFunc")} (jnienv, native__this";
foreach (var p in method.Parameters)
call += $", {opt.GetSafeIdentifier (p.UnsafeNativeName)}";
call += $", &__{Name})";

writer.WriteLine ("unsafe {");
writer.Indent ();
writer.WriteLine (method.IsVoid ? "return;" : "return default;");
writer.WriteLine (method.IsVoid ? call + ";" : "return " + call + ";");
writer.Unindent ();
writer.WriteLine ("}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Performance — The call string is built by repeated += concatenation in a loop over method.Parameters. For methods with many parameters, this creates intermediate string allocations on every iteration. Using a StringBuilder (or a single interpolated expression with string.Join) would be more efficient:

var paramArgs = string.Join ("", method.Parameters.Select (p => $", {opt.GetSafeIdentifier (p.UnsafeNativeName)}"));
var call = $"global::Java.Interop.JniMarshal.{(method.IsVoid ? "SafeInvokeAction" : "SafeInvokeFunc")} (jnienv, native__this{paramArgs}, &__{Name})";

This is a build-time code generator so the runtime impact is negligible, but it's cleaner.

Rule: Avoid unnecessary allocations

public static unsafe TResult SafeInvokeFunc<TResult> (IntPtr jnienv, IntPtr self, delegate* managed<IntPtr, IntPtr, TResult> action)
{
if (!JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r))
return default!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Nullabledefault! uses the null-forgiving operator, which is generally banned per repo conventions. The old generated code used return default; without !. Here it's arguably justified — after OnUserUnhandledException forwards the exception to the JVM, the return value is dead — but it may warrant a brief comment explaining why ! is needed (the generic TResult can be a non-nullable reference type under NRT, but the value is never observed by the caller).

This pattern appears in all 17 SafeInvokeFunc overloads (both in the BeginMarshalMethod early-return and in the catch block).

Rule: Never use ! (null-forgiving operator)

Comment on lines +52 to +70
public static unsafe void SafeInvokeAction (IntPtr jnienv, IntPtr self, delegate* managed<IntPtr, IntPtr, void> action)
{
if (!JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r))
return;

try {
action (jnienv, self);
} catch (Exception __e) {
__r.OnUserUnhandledException (ref __envp, __e);
} finally {
JniEnvironment.EndMarshalMethod (ref __envp);
}
}

[DebuggerDisableUserUnhandledExceptions]
public static unsafe TResult SafeInvokeFunc<TResult> (IntPtr jnienv, IntPtr self, delegate* managed<IntPtr, IntPtr, TResult> action)
{
if (!JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r))
return default!;

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 — These 34 overloads (17 arities × Action/Func) span ~530 lines of identical boilerplate differing only in type parameter count. Consider generating them from a T4 template (similar to the existing JniBuiltinMarshalers.tt pattern) to reduce maintenance burden and eliminate the risk of copy-paste inconsistencies across overloads.

Rule: Code organization — use code generation for repetitive patterns

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, it's probably worth using a .tt file like this repo does for some other things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce IL size in generator output

3 participants