Android framework version
net11.0-android (Preview)
Affected platform version
dotnet/android main; generated C# bindings across supported target frameworks and runtimes.
Description
Generated C# bindings currently use a try/finally block even when the finally contains only GC.KeepAlive() calls for Java reference arguments. Exception-handling regions prevent RyuJIT from inlining otherwise-thin binding wrappers and add EH metadata/code size.
When no marshaling cleanup is required, the generator could instead emit:
- The JNI invocation, storing any return value in a local.
GC.KeepAlive() for each managed argument that supplied a borrowed JNI handle.
- Return the stored result.
For example:
var result = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, __args);
GC.KeepAlive (value);
return result;
The existing try/finally shape should remain whenever the generated method must delete local JNI references, dispose marshaler state, unpin memory, copy arrays back, or perform any other cleanup on exceptional exits.
This should be evaluated for generated C# bindings to Java APIs, including methods, properties, and constructors. The change should preserve argument and receiver lifetime guarantees related to #5405 across MonoVM, CoreCLR, and NativeAOT.
Validation should include generator expected-output tests, concurrent-GC lifetime stress coverage, exception-path coverage, and benchmarks or generated-IL/code-size comparisons demonstrating whether removing keep-alive-only EH regions enables useful inlining or otherwise reduces overhead.
Steps to Reproduce
- Generate a C# binding method with a Java reference parameter that requires no temporary local-reference cleanup.
- Inspect the generated method.
- Observe that the JNI invocation is wrapped in
try/finally solely to execute GC.KeepAlive(parameter).
- Inspect the resulting IL/JIT diagnostics and observe the EH clause, which prevents method inlining.
Did you find any workaround?
Handwritten bindings can store the JNI result in a local, call GC.KeepAlive() after the invocation, and then return the result. There is no project-wide workaround for generated bindings.
Relevant log output
N/A
Android framework version
net11.0-android (Preview)
Affected platform version
dotnet/androidmain; generated C# bindings across supported target frameworks and runtimes.Description
Generated C# bindings currently use a
try/finallyblock even when thefinallycontains onlyGC.KeepAlive()calls for Java reference arguments. Exception-handling regions prevent RyuJIT from inlining otherwise-thin binding wrappers and add EH metadata/code size.When no marshaling cleanup is required, the generator could instead emit:
GC.KeepAlive()for each managed argument that supplied a borrowed JNI handle.For example:
The existing
try/finallyshape should remain whenever the generated method must delete local JNI references, dispose marshaler state, unpin memory, copy arrays back, or perform any other cleanup on exceptional exits.This should be evaluated for generated C# bindings to Java APIs, including methods, properties, and constructors. The change should preserve argument and receiver lifetime guarantees related to #5405 across MonoVM, CoreCLR, and NativeAOT.
Validation should include generator expected-output tests, concurrent-GC lifetime stress coverage, exception-path coverage, and benchmarks or generated-IL/code-size comparisons demonstrating whether removing keep-alive-only EH regions enables useful inlining or otherwise reduces overhead.
Steps to Reproduce
try/finallysolely to executeGC.KeepAlive(parameter).Did you find any workaround?
Handwritten bindings can store the JNI result in a local, call
GC.KeepAlive()after the invocation, and then return the result. There is no project-wide workaround for generated bindings.Relevant log output
N/A