Commit f63eab9
Reduce generated JNI callback IL size (#1452)
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:
```csharp
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
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>1 parent a961e99 commit f63eab9
69 files changed
Lines changed: 1802 additions & 1816 deletions
File tree
- external/Java.Interop
- src/Java.Interop
- Java.Interop
- tests/generator-Tests
- Unit-Tests/CodeGeneratorExpectedResults
- XAJavaInterop1-NRT
- XAJavaInterop1
- expected.xaji
- AccessModifiers
- Adapters
- Android.Graphics.Color
- CSharpKeywords
- Core_Jar2Xml
- GenericArguments
- InterfaceMethodsConflict
- NestedTypes
- NormalMethods
- NormalProperties
- ParameterXPath
- Streams
- TestInterface
- java.lang.Enum
- tools/generator/SourceWriters
- Attributes
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
63 | 67 | | |
64 | 68 | | |
65 | 69 | | |
| |||
86 | 90 | | |
87 | 91 | | |
88 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
89 | 96 | | |
90 | 97 | | |
91 | 98 | | |
| |||
0 commit comments