Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

marshal-ilgen component should be optional #7276

Open
jonpryor opened this issue Aug 17, 2022 · 2 comments
Open

marshal-ilgen component should be optional #7276

jonpryor opened this issue Aug 17, 2022 · 2 comments
Assignees
Labels
Area: App+Library Build Issues when building Library projects or Application projects. enhancement Proposed change to current functionality.
Milestone

Comments

@jonpryor
Copy link
Member

jonpryor commented Aug 17, 2022

Android application type

Android for .NET (net6.0-android, etc.)

Affected platform version

.NET 7

Description

With #7249 and #7260, the marshal-ilgen component is always included into Android apps.

For .NET 8 (?), we should try to make inclusion of this component optional, such that it is included only if required.

As a prerequisite, this will require:

  1. updating Mono.Androi.dll and Java.Interop.dll (and others?) to use the DllImport Generator.
  2. Updating the above assemblies to have [assembly:DisableRuntimeMarshallingAttribute]

Then, we'd need to update the App Build process to scan all included assemblies, and if all included assemblies contain [assembly:DisableRuntimeMarshallingAttribute], then the marshal-ilgen component can be removed from the .apk.

@jonpryor jonpryor added Area: App+Library Build Issues when building Library projects or Application projects. needs-triage Issues that need to be assigned. labels Aug 17, 2022
@jonpryor jonpryor added this to the .NET 8 milestone Aug 17, 2022
jonathanpeppers pushed a commit to jonathanpeppers/xamarin-android that referenced this issue Aug 17, 2022
Fixes: dotnet#7249

Context: dotnet/runtime#71203
Context: dotnet/runtime#60639
Context: dotnet/runtime#61685
Context: dotnet#7276

Mono introduced a new `marshal-ilgen` component in
dotnet/runtime@de32c446 which needs to be used when
*non-blittable type* is required.

`string` marshaling is a form of non-blittable marshaling, as is
`Marshal.PtrToStructure()` (?!).

Eventually the idea is that assemblies should migrate to use the
[`DllImport Generator`][0] for non-blittable marshaling purposes,
then [`[assembly:DisableRuntimeMarshallingAttribute]`][1] can be
applied to the assembly, and -- if no assemblies in an Android app
require non-blittable marshaling -- then the `marshal-ilgen`
component can be omitted from the `.apk`, reducing app size.

That's a fair number of `if`s; this won't be happening soon.

In the meantime, we need to start including the `marshal-ilgen`
component in all .NET 7 RC1+ builds so that our existing
`[DllImport]` declarations continue to work.  Failure to do so
results in runtime errors:

	mono-rt : [ERROR] FATAL UNHANDLED EXCEPTION: System.InvalidProgramException: Invalid IL code in (wrapper managed-to-native) object:wrapper_native_0x7725af156f10 (intptr): IL_0012: calli     0x00000003
	mono-rt :    at System.Runtime.InteropServices.Marshal.PtrToStructureHelper(IntPtr , Object , Boolean )
	mono-rt :    at System.Runtime.InteropServices.Marshal.PtrToStructure(IntPtr , Type )
	mono-rt :    at Java.Interop.JniRuntime.CreateInvoker(IntPtr )
	mono-rt :    at Java.Interop.JniRuntime..ctor(CreationOptions )
	mono-rt :    at Android.Runtime.AndroidRuntime..ctor(IntPtr , IntPtr , Boolean , IntPtr , IntPtr , Boolean )
	mono-rt :    at Android.Runtime.JNIEnv.Initialize(JnienvInitializeArgs* )

Update `@(_MonoComponent)` to add `marshal-ilgen`.

In order to facilitate future testing, allow the `marshal-ilgen`
component to be *excluded* if the
`$(_AndroidExcludeMarshalIlgenComponent)` property is True.

[0]: https://github.com/dotnet/runtimelab/tree/feature/DllImportGenerator
[1]: dotnet/runtime#60639
@dellis1972
Copy link
Contributor

So is the plan for users to add this attribute to their own assemblies? I assume this will only need to check on assemblies which reference Mono.Android? or does it apply to every single assembly ?

@jonpryor
Copy link
Member Author

@dellis1972 asked:

is the plan for users to add this attribute to their own assemblies?

My interpretation is that yes, any/every assembly which contains P/Invokes would need to opt-in to this new mechanism by adding [assembly:DisableRuntimeMarshallingAttribute]. This would not be restricted to just assemblies which reference Mono.Android.dll.

Which possibly raises as many questions as that answers. How does one efficiently tell if an assembly contains no P/Invokes?

@jpobst jpobst removed the needs-triage Issues that need to be assigned. label Aug 23, 2022
@jonpryor jonpryor modified the milestones: .NET 8, .NET 9 Planning Mar 27, 2023
@jpobst jpobst added the enhancement Proposed change to current functionality. label Oct 10, 2023
jonpryor added a commit to dotnet/java-interop that referenced this issue Mar 29, 2024
Context: c6c487b
Context: 312fbf4
Context: 2197579
Context: dotnet/android#7276

There is a desire to remove the "marshal-ilgen" component from
.NET Android, which is responsible for all non-blittable type
marshaling within P/Invoke (and related) invocations.

The largest source of such non-blittable parameter marshaling was
with string marshaling: `JNIEnv::GetFieldID()` was "wrapped" by
`java_interop_jnienv_get_field_id`:

	JI_API jfieldID java_interop_jnienv_get_field_id (JNIEnv *env, jthrowable *_thrown, jclass type, const char* name, const char* signature);

which was P/Invoked within `JniEnvironment.g.cs`:

	partial class NativeMethods {
	    internal static extern unsafe IntPtr java_interop_jnienv_get_field_id (IntPtr jnienv, out IntPtr thrown, jobject type, string name, string signature);
	}

and `string` parameter marshaling is *not* blittable.

Turns out™ that this particular usage of non-blittable parameter
marshaling was fixed and rendered moot by:

  * 312fbf4: C#9 function pointer backend for `JNIEnv` invocations
  * c6c487b: "Standalone" build config to use C#9 function pointers
  * 2197579: Standalone build config is now the default

That said, this code path felt slightly less than ideal: the
"top-level abstraction" for member lookups is an "encoded member",
a string containing the name of the member, a `.`, and the JNI
signature of the member, e.g.:

	_members.InstanceFields.GetBooleanValue("propogateFinallyBlockExecuted.Z", this)

The "encoded member" would need to be split on `.`, and with c6c487b
the name and signature would be separately passed to
`Marshal.StringToCoTaskMemUTF8()`, which performs a memory allocation
and converts the UTF-16 string to UTF-8.

Meanwhile, [C# 11 introduced UTF-8 string literals][0], which allows
the compiler to deal with UTF-8 conversion and memory allocation.

Enter `JniMemberInfoLookup``:

	public ref struct JniMemberInfoLookup {
	    public  string                  EncodedMember   {get;}
	    public  ReadOnlySpan<byte>      MemberName      {get;}
	    public  ReadOnlySpan<byte>      MemberSignature {get;}

	    public JniMemberInfoLookup (string encodedMember, ReadOnlySpan<byte> memberName, ReadOnlySpan<byte> memberSignature);
	}

`JniMemberInfoLookup` removes the need to call
`Marshal.StringToCoTaskMemUTF8()` entirely, at the cost of a more
complicated member invocation:

	// Old and busted:
	bool value = _members.InstanceFields.GetBooleanValue("propogateFinallyBlockExecuted.Z", this);

	// Eventual new hawtness:
	var lookup = new JniMemberInfoLookup (
		"propogateFinallyBlockExecuted.Z",
		"propogateFinallyBlockExecuted"u8,
		"Z"u8);
	bool value = _members.InstanceFields.GetBooleanValue(lookup, this);

Is It Worth It™?  *Maybe*; see the new
`JniFieldLookupTiming.FieldLookupTiming()` test, which allocates a new
`JniPeerMembers` instance and invoke
`members.InstanceFields.GetFieldInfo(string)` and
`members.InstanceFields.GetFieldInfo(JniMemberInfoLookup)`.
(A new `JniPeerMembers` instance is required because `GetFieldInfo()`
caches the field lookup.)  Using `JniMemberInfoLookup` is about
4% faster.

	# FieldLookupTiming Timing: looking up JavaTiming.instanceIntField 10000 times
	#   .InstanceMethods.GetFieldInfo(string):              00:00:02.2780667
	#   .InstanceMethods.GetFieldInfo(JniMemberInfoLookup): 00:00:02.2016146

I'm not sure if this is *actually* worth it, especially as this will
imply an increase in code size.

TODO:

  * Update `JniPeerMembers.*.cs` to use `JniMemberInfoLookup`, so
    that e.g. the above `_members.InstanceFields.GetBooleanValue()`
    overload exists.

  * `generator` changes to use `JniMemberInfoLookup`

[0]: https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-11#utf-8-string-literals
jonpryor added a commit to dotnet/java-interop that referenced this issue Jul 9, 2024
Context: c6c487b
Context: 312fbf4
Context: 2197579
Context: dotnet/android#7276

There is a desire to remove the "marshal-ilgen" component from
.NET Android, which is responsible for all non-blittable type
marshaling within P/Invoke (and related) invocations.

The largest source of such non-blittable parameter marshaling was
with string marshaling: `JNIEnv::GetFieldID()` was "wrapped" by
`java_interop_jnienv_get_field_id`:

	JI_API jfieldID java_interop_jnienv_get_field_id (JNIEnv *env, jthrowable *_thrown, jclass type, const char* name, const char* signature);

which was P/Invoked within `JniEnvironment.g.cs`:

	partial class NativeMethods {
	    internal static extern unsafe IntPtr java_interop_jnienv_get_field_id (IntPtr jnienv, out IntPtr thrown, jobject type, string name, string signature);
	}

and `string` parameter marshaling is *not* blittable.

Turns out™ that this particular usage of non-blittable parameter
marshaling was fixed and rendered moot by:

  * 312fbf4: C#9 function pointer backend for `JNIEnv` invocations
  * c6c487b: "Standalone" build config to use C#9 function pointers
  * 2197579: Standalone build config is now the default

That said, this code path felt slightly less than ideal: the
"top-level abstraction" for member lookups is an "encoded member",
a string containing the name of the member, a `.`, and the JNI
signature of the member, e.g.:

	_members.InstanceFields.GetBooleanValue("propogateFinallyBlockExecuted.Z", this)

The "encoded member" would need to be split on `.`, and with c6c487b
the name and signature would be separately passed to
`Marshal.StringToCoTaskMemUTF8()`, which performs a memory allocation
and converts the UTF-16 string to UTF-8.

Meanwhile, [C# 11 introduced UTF-8 string literals][0], which allows
the compiler to deal with UTF-8 conversion and memory allocation.

Enter `JniMemberInfoLookup``:

	public ref struct JniMemberInfoLookup {
	    public  string                  EncodedMember   {get;}
	    public  ReadOnlySpan<byte>      MemberName      {get;}
	    public  ReadOnlySpan<byte>      MemberSignature {get;}

	    public JniMemberInfoLookup (string encodedMember, ReadOnlySpan<byte> memberName, ReadOnlySpan<byte> memberSignature);
	}

`JniMemberInfoLookup` removes the need to call
`Marshal.StringToCoTaskMemUTF8()` entirely, at the cost of a more
complicated member invocation:

	// Old and busted:
	bool value = _members.InstanceFields.GetBooleanValue("propogateFinallyBlockExecuted.Z", this);

	// Eventual new hawtness:
	var lookup = new JniMemberInfoLookup (
		"propogateFinallyBlockExecuted.Z",
		"propogateFinallyBlockExecuted"u8,
		"Z"u8);
	bool value = _members.InstanceFields.GetBooleanValue(lookup, this);

Is It Worth It™?  *Maybe*; see the new
`JniFieldLookupTiming.FieldLookupTiming()` test, which allocates a new
`JniPeerMembers` instance and invoke
`members.InstanceFields.GetFieldInfo(string)` and
`members.InstanceFields.GetFieldInfo(JniMemberInfoLookup)`.
(A new `JniPeerMembers` instance is required because `GetFieldInfo()`
caches the field lookup.)  Using `JniMemberInfoLookup` is about
4% faster.

	# FieldLookupTiming Timing: looking up JavaTiming.instanceIntField 10000 times
	#   .InstanceMethods.GetFieldInfo(string):              00:00:02.2780667
	#   .InstanceMethods.GetFieldInfo(JniMemberInfoLookup): 00:00:02.2016146

I'm not sure if this is *actually* worth it, especially as this will
imply an increase in code size.

TODO:

  * Update `JniPeerMembers.*.cs` to use `JniMemberInfoLookup`, so
    that e.g. the above `_members.InstanceFields.GetBooleanValue()`
    overload exists.

  * `generator` changes to use `JniMemberInfoLookup`

[0]: https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-11#utf-8-string-literals
@jonpryor jonpryor modified the milestones: .NET 9, .NET 10 Aug 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: App+Library Build Issues when building Library projects or Application projects. enhancement Proposed change to current functionality.
Projects
None yet
Development

No branches or pull requests

4 participants