Conversation
|
|
||
| - Debug and Release builds using CoreCLR or NativeAOT runtime | ||
| - All Java peer types: user classes, SDK bindings, interfaces with invokers | ||
| - `[Register]` and `[Export]` attribute methods |
There was a problem hiding this comment.
Note that we are already considering [Export] not trimmer safe at all -- it currently emits trimmer warnings.
It isn't a required feature for Android, as you can just make an interface instead, and the entire API will be strongly typed. [Export] is basically C# -> strings -> generate a Java class/method with your string.
An example of removing [Export]:
So, we could consider improving [Export] future, out of scope.
|
I think this looks great, and I don't see any major problems for macios either! |
| **Key Insight:** In the legacy system, most types are only preserved if they're referenced by user code. The legacy `MarkJavaObjects` only unconditionally marks: | ||
| 1. Types with `[Activity]`, `[Service]`, `[BroadcastReceiver]`, `[ContentProvider]`, `[Application]`, `[Instrumentation]` attributes |
There was a problem hiding this comment.
I guess you could technically, not use these attributes and put them in your AndroidManifest.xml file manually.
That means that AndroidManifest.xml can define additional "roots" other than the application assembly. I don't think people would commonly do this, but maybe we should mention it's not trimmer safe -- they'd need to preserve the type another way.
There was a problem hiding this comment.
We already scan layouts and root the types that appear in those XML files. We can easily scan AndroidManifest.xml as well.
- Rename JavaPeerContainerFactory → DerivedTypeFactory to match implementation - Rename GetContainerFactory() → GetDerivedTypeFactory() - Add Integration with JavaConvert section explaining how IList<T>, IDictionary<K,V>, and ICollection<T> use DerivedTypeFactory via TypeMap - Add Primitive/String Element Types section explaining direct converters - Update section cross-references
…ctory" This reverts commit 6d4c273.
- Updated API to reflect simplified methods (removed unused Set/empty creators) - Removed FromHandle suffix from method names - Added 'PoC Usage' sections showing exactly where the factory is used: - TypeMapAttributeTypeMap.CreateArray for array marshalling - JavaConvert for IList<T>, ICollection<T>, IDictionary<K,V> marshalling - Explained why primitives need explicit converters (no proxies in TypeMap) - Updated supported container types table
Document the gap where manually-added manifest entries (without C# attributes) may be trimmed. Propose scanning merged AndroidManifest.xml for component references (activity, service, receiver, provider, backupAgent) similar to how layout XML is scanned for custom views. This addresses @jonathanpeppers' review comment about manual manifest entries.
Covers key Native AOT considerations: - Why Native AOT differs from ILLink+MonoVM (ILC whole-program analysis) - Forbidden patterns (MakeGenericType, Activator.CreateInstance, Array.CreateInstance) - JNI callback implementation with UnmanagedCallersOnly - Symbol export requirements for JNI methods - Crypto/TLS integration (whole-archive, JNI init, ProGuard rules) - TypeMap runtime initialization timing - Build pipeline differences - Debugging common failure patterns - Future considerations (.NET 10+ TypeMapping API) Focuses on reasoning and 'why' rather than implementation details.
| | Component | Location | Responsibility | | ||
| |-----------|----------|----------------| | ||
| | `GenerateTypeMaps` | MSBuild task | Scan assemblies, generate TypeMapAssembly.dll, .java, .ll | | ||
| | `TypeMapAssembly.dll` | Generated | Contains proxies, UCOs, TypeMap attributes | |
There was a problem hiding this comment.
So, one question about this assembly. If we kept 100% compatibility with existing code, then it would need to:
- Potentially access
internaltypes in other assemblies - Access nested private/internal types
Today someone can make nested/private/internal types that extend Java.Lang.Object and they work.
Is there some discussion in here how to solve this problem?
There was a problem hiding this comment.
Elaborating on this, today there are nested protected internal types within Mono.Android.dll which can be subclassed, e.g. Android.Content.AsyncQueryHandler.WorkerArgs.
It is also not required for new Java.Lang.Object subclasses to be public; internal is perfectly valid today:
internal class Whatever : Java.Lang.Object {
}A possible solution would be a trimmer step which just makes all types public.
There was a problem hiding this comment.
We only need to generate [assembly: IgnoresAccessChecksTo("...")] in the typemap assembly for each assembly it references and we can call private methods on private classes. That alone fixes the problem. I verified this in PoC. I was also testing [UnsafeAccessors(...)] and it would be also a working solution to this problem.
|
I don’t think we need the llvm il stuff or anything mono aot related — we aren’t changing the old system so I think it should work fine for older tfms without modification. |
| │ │ │ │ │ | ||
| │ ▼ ▼ ▼ │ | ||
| │ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐│ | ||
| │ │ ILLink/Trimmer │ │ Java Compiler │ │ LLVM → .o → .so ││ |
There was a problem hiding this comment.
For Native AOT, LLVM output should compile to a .a which is later used as input by the Native AOT toolchain.
|
|
||
| ### 3.1 High-Level Design | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Markdown is a superset of HTML. Why not just use HTML tables?
| │ │ │ │ │ | ||
| │ ▼ ▼ ▼ │ | ||
| │ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐│ | ||
| │ │ ILLink/Trimmer │ │ Java Compiler │ │ LLVM → .o → .so ││ |
There was a problem hiding this comment.
For Native AOT, LLVM output should compile to a .a which is later used as input by the Native AOT toolchain. I'm not sure where
| ┌─────────────────────────────────────────────────────────────────────────────┐ | ||
| │ RUNTIME │ | ||
| ├─────────────────────────────────────────────────────────────────────────────┤ | ||
| │ │ |
There was a problem hiding this comment.
We need a "step 0" and elaboration of setup for Java > Native invocations.
Background: dotnet/java-interop@356485e
The Java Native Interface allows native code to be associated
with a Javanativemethod declaration, either by way of
Java_-prefixed native functions, or via function pointers
provided toJNIEnv::RegisterNatives().
The fact that you're mentioning LLVM → .o → .so strongly suggests that you're planning on requiring Java_-prefixed native functions.
Firstly, the fact that there is a choice and that you're making it should be explicitly mentioned somewhere.
Secondly, in order for Java_-prefixed symbols to work, something needs to load the .so before the Java native method is invoked.
(In the case of NativeAOT, if the symbols are instead part of a .a which is linked into libApp.so, then the .so does not need to be explicitly loaded, as libApp.so is already loaded as part of bootstrap.)
I see no mention of System.loadLibrary() within the current document. Presumably mono/android/MonoPackageManager.java will need to be updated to load this lib.
There was a problem hiding this comment.
Thanks for pointing this out. I want to use the Java_-prefixed symbols. I will update the spec to clearly state that we need to call System.loadLibrary().
| bool TryGetTypesForJniName(string jniSimpleReference, [NotNullWhen(true)] out IEnumerable<Type>? types); | ||
|
|
||
| // .NET-to-Java type resolution | ||
| bool TryGetJniNameForType(Type type, [NotNullWhen(true)] out string? jniName); |
There was a problem hiding this comment.
API design curiosity: why an out string? parameter instead of a string? return type? I haven't looked to see if the .NET Framework Design Guidelines have been updated for nullability in a way that addresses this question.
There was a problem hiding this comment.
API design naming: if both (Try)GetJniNameForType(Type [, out string?]) and GetJniNamesForType() are kept, it may be useful to insert an adjective on the "singular' overload, e.g. GetDefaultJniNameForType().
There was a problem hiding this comment.
Both designs would work just fine. I prefer the Try variant because it's closer to how Dictionary lookups are used. It's just more explicit. This API is not meant for public consumption and it should be an internal interface, I should mention that as well.
|
|
||
| // .NET-to-Java type resolution | ||
| bool TryGetJniNameForType(Type type, [NotNullWhen(true)] out string? jniName); | ||
| IEnumerable<string> GetJniNamesForType(Type type); |
There was a problem hiding this comment.
API design question: when would you call this instead of TryGetJniNameForType()? Looking at https://github.com/dotnet/android/compare/dev/simonrozsival/trimmable-typemap , the apparent answer is when implementing GetSimpleReferences(), but as all GetJniNamesForType() return a single-element array, I'm not sure how this differs.
The "real" answer is because of JniRuntime.JniTypeManager.GetSimpleReferences(), but I can no longer remember why this needs to provide a 1:many for System.Type : JNI values. ("Aliases" are the other direction, for JNI : Type mappings, as multiple types may bind e.g. java.lang.Object.)
It may be "interesting" to just forego this method entirely and see what breaks.
There was a problem hiding this comment.
This is only because of JniRuntime.JniTypeManager.GetSimpleReferences(). One option would be to deprecate that public method and skip the implementation for this proposed typemap.
Could this be needed for "MAM" type replacement?
There was a problem hiding this comment.
| IEnumerable<string> GetJniNamesForType(Type type); |
| IJavaPeerable? CreatePeer(IntPtr handle, JniHandleOwnership transfer, Type? targetType); | ||
|
|
||
| // Marshal method function pointer resolution | ||
| IntPtr GetFunctionPointer(ReadOnlySpan<char> className, int methodIndex); |
There was a problem hiding this comment.
Naming + clarification: what is className? The Java class name or the managed (C#) class name? The name should clearly tell us which is needed.
There was a problem hiding this comment.
| IntPtr GetFunctionPointer(ReadOnlySpan<char> className, int methodIndex); | |
| IntPtr GetFunctionPointer(ReadOnlySpan<char> jniName, int methodIndex); |
| else if (RuntimeFeature.IsMonoRuntime) | ||
| return new MonoTypeMap(); | ||
| else | ||
| throw new NotSupportedException(); |
There was a problem hiding this comment.
Seems somewhat odd that Native AOT isn't mentioned here, while it is mentioned elsewhere…
There was a problem hiding this comment.
This is an oversight. It should be if (RuntimeFeature.IsCoreClrRuntime || RuntimeFeature.IsNativeAotRuntime)
|
|
||
| | Type Category | Example | JCW? | TypeMap Entry | GetFunctionPointer | CreateInstance | | ||
| |---------------|---------|------|---------------|-------------------|----------------| | ||
| | User class with JCW | `MainActivity` | Yes | ✅ | Returns UCO ptrs | `new T(h, t)` | |
There was a problem hiding this comment.
How would new T(h, t) work? Current convention is that most user-written classes do not have the (IntPtr, JniHandleOwnership) "activation constructor", e.g.
[Activity(…)]
public partial class MainActivity {
// No explicit constructor at all! which means it only has a compiler-provided default constructor.
}I imagine this can work by updating the IL to add such a constructor… But such IL rewriting tends to be a source of pain and suffering in the context of incremental builds.
There was a problem hiding this comment.
Alternatively, since this is CreateInstance() on the proxy type, this doesn't need to be new T(h, t), but it can instead be RuntimeHelpers.GetUninitializedObject) + .SetPeerReference(h) + ConstructorInfo.Invoke(instance). Though you'd have to somehow obtain a ConstructorInfo in a NativeAOT environment…
There was a problem hiding this comment.
right, this is an oversimplification and I believe this is described later on, I need to doublecheck. The generated code will need to match the current reflection-based implementation, which uses GetUninitializedObject() + direct call to the (protected) base class .ctor(h, t). This can be achieved easily in IL + [assembly: IgnoresAccessChecksTo("...")]
| |---------------|---------|--------| | ||
| | Invoker | `IOnClickListenerInvoker` | Share JNI name with interface; instantiated by interface proxy | | ||
| | `DoNotGenerateAcw` types without activation | Internal helpers | No JCW, no peer creation from Java | | ||
| | Generic types | `List<T>` | Not directly mapped to Java | |
There was a problem hiding this comment.
This requires elaboration. Generic types can be exposed to Java!
CreateInstance() cannot be implemented in this scenario, and already throws an exception should you try to do so, but other ITypeMap methods can be implemented for GenericHolder<T>.
There was a problem hiding this comment.
Thanks for pointing this out. Yes, there's no way to implement CreateInstance, so we won't. The object needs to be created on the .NET side and as you said, we can later obtain a reference to it through GetObject<GenericHolder<T>>(ptr). I will update the spec to explicitly mention that generic subclasses are supported in this way.
|
|
||
| --- | ||
|
|
||
| ## 5. Type Map Attributes |
There was a problem hiding this comment.
Should this section come after §6 JavaPeerProxy Design so that IAndroidCallableWrapper "exists" by the time we see it used in MainActivity_Proxy?
| [assembly: TypeMap<Java.Lang.Object>("com/example/MainActivity", typeof(MainActivity_Proxy), typeof(MainActivity))] | ||
| ``` | ||
|
|
There was a problem hiding this comment.
"obvious questions are obvious":
What emits this attribute, where is it located? Is it part of TypeMapAssembly.dll? It it (ever) hand-written or part of generator output?
(Asked earlier) How does this handle non-public types?
There was a problem hiding this comment.
I will update the document to make it obvious that these are generated alongside teh Proxy types in the pre-trimming codegen step and are part of the "TypeMapAssembly.dll". It will never be hand-written.
This will be generated directly in IL, so we can emit typerefs even to private types. As I've written in another comment, we also need to rely on [assembly: IgnoresAccessChecksTo("...")]
| public sealed class MainActivity_Proxy : JavaPeerProxy, IAndroidCallableWrapper | ||
| { | ||
| public override IJavaPeerable CreateInstance(IntPtr handle, JniHandleOwnership transfer) | ||
| => new MainActivity(handle, transfer); |
There was a problem hiding this comment.
As mentioned above, there likely is not a MainActivity(IntPtr, JniHandleOwnership) constructor. How does this work?
There was a problem hiding this comment.
Answered in another thread
|
|
||
| ```csharp | ||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, Inherited = false)] | ||
| abstract class JavaPeerProxy : Attribute |
There was a problem hiding this comment.
This confuses me, actually: .NET Framework Design Guidelines is that Attribute subclasses should have an Attribute suffix. Yet here we don't.
Additionally, the lack of an Attribute suffix makes me think I've been mis-reading JavaPeerProxy usage before I made this realization…
There was a problem hiding this comment.
I see. The JavaPeerProxy class should probably have the Attribute suffix, since we need to refer to this class in the codebase and I understand your confusion. For the generated subclasses, the name doesn't really matter and they are never referenced by name in code.
I don't think about this class as an attribute most time. It's basically a hack which allows to safely create instance of this class when we only have a Type reference. I suppose we might just as well add [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] on JavaPeerProxy and use Activator.CreateInstance on it. The end result will be mostly the same.
| // At runtime: | ||
| Type proxyType = typeMap["com/example/MainActivity"]; // Returns typeof(MainActivity_Proxy) | ||
| JavaPeerProxy proxy = proxyType.GetCustomAttribute<JavaPeerProxy>(); // Returns MainActivity_Proxy instance | ||
| IJavaPeerable instance = proxy.CreateInstance(handle, transfer); // Returns MainActivity instance |
There was a problem hiding this comment.
What this "at runtime" example doesn't describe is how "activation" is supposed to work, mentioned in: dotnet/java-interop@d3d3a1b
(Alas, terminology is inconsistent; we have an (IntPtr, JniHandleOwnership) "activation constructor", which is not the TypeManager.n_Activate() codepath:
Because you love MainActivity… 😉
Given:
namespace Example;
[Activity(…)]
public partial class MainActivity : Activity {
public MainActivity() => Console.WriteLine("MainActivity constructed!");
// Note: no (IntPtr, JniHandleOwnership) "activation" constructor present!
}
The Java Callable Wrapper resembles:
package example;
public class MainActivity extends android.app.Activity implements mono.android.IGCUserPeer
{
/** @hide */
public static final String __md_methods;
static {
__md_methods = "…";
mono.android.Runtime.register ("android_v9_intune.MainActivity, android-v9-intune", MainActivity.class, __md_methods);
}
public MainActivity ()
{
super ();
if (getClass () == MainActivity.class) {
mono.android.TypeManager.Activate ("android_v9_intune.MainActivity, android-v9-intune", "", this, new java.lang.Object[] { });
}
}
// …
}
What happens when a user taps the app icon on their home screen?
-
Android "does stuff" to map the icon to an app + Java class name.
-
Once it has a
java.lang.Classfor (1), it callsClass.newInstance() -
(2) is equivalent to
new example.MainActivity() -
The
example.MainActivitydefault constructor executes. Note: at this time, (1)…(4) are all in Java-land. No .NET code has executed (except thestaticinitializer andRuntime.register(), if it hasn't already executed.) -
The
MainActivitydefault constructor callsTypeManager.Activate(…, this, …). -
Now we enter managed-land with
TypeManager.Activate(). which has these implied semantics:-
An instance of the C#
Example.MainActivitytype is created. -
The
.Handlevalue from (a) is the "same" JNI handle provided toTypeManager.Activate().(i) and (ii) could plausibly be done via
proxy.CreateInstance(handle, transfer), but then! -
The corresponding C# constructor is then invoked on the instance from (i)
-
Meaning that by the end of 6.iii adb logcat had better contain MainActivity constructed!, or things are broken.
How do you implement 6.iii?
Additionally, this happens for all Java-side construction. We are not restricted to just default constructors. (See also CallVirtualFromConstructorBase.cs and CallVirtualFromConstructorDerived.cs.)
Additionally, constructors can be overloaded; there need not be just one.
A spitballed solution is for CreateInstance() to instead be:
IJavaPeerable? CreateInstance(IntPtr handle, JniHandleOwnership transfer, ReadOnlySpan<char> jniMethodSignature, JniObjectArray? arguments);
Generated IL could then be of the form:
public IJavaPeerable? CreateInstance(IntPtr handle, JniHandleOwnership transfer, ReadOnlySpan<char> jniMethodSignature, JniObjectArray? arguments)
{
if (!jniMethodSignature.Equals("()V")) return null;
var value = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject(typeof(MainActivity));
value.SetPeerReference(new PeerReference(handle));
/* the IL form of */ MainActivity::.ctor(value);
return value;
}
At a high level, this feels viable. Implementation wise, you're gonna need to deal with type coercion, e,g. if the C# constructor is MyType(int value), then arguments[0] would contain a java.lang.Integer value which needs to be converted to a System.Int32.
This can be done. But this vastly increases the complexity of your CreateInstance() method pattern.
There was a problem hiding this comment.
Upon further reading (the joys of commenting while reading…), the MainActivity_Proxy.nc_activate_0() method also fulfills the "Java-side activation" scenario.
However, it wasn't explicitly called out! 😉
Additionally, it's presence implies changes to Java Callable Wrappers:
package example;
public class MainActivity extends android.app.Activity implements mono.android.IGCUserPeer
{
/** @hide */
public static final String __md_methods;
static {
__md_methods = "…";
mono.android.Runtime.register ("android_v9_intune.MainActivity, android-v9-intune", MainActivity.class, __md_methods);
}
public MainActivity ()
{
super ();
if (getClass () == MainActivity.class) {
nc_activate();
}
}
native void nc_activate();
public MainActivity (int value) // overload for exposition
{
super ();
if (getClass () == MainActivity.class) {
nc_activate(value);
}
}
native void nc_activate(int value);
// …
}Which should work, and makes the "argument marshaling" scenario easier (everything isn't boxed into a Java-side Object[]!), but does mean that JCWs contents now vary based on selected runtime. This would also need to be called out.
There was a problem hiding this comment.
You got it right in the second comment, I think. Let me break it down:
- Yes, I would like to make registered (exported?) ctors callable via the same mechanism as any other reverse p/invoke and avoid the reflection codepath in
TypeManager.Activate. - So there are 2 types of constructors there's the "XI"
(IntPtr, JniTransferOptions)and "JI"(ref JniPeerReference, ???)that should be invoked fromObject.GetObject(javaThis)- that's theJavaPeerProxy.CreateInstancecodepaths. And the second group are the other "registered" ("exported"?) constructors that are called viaTypeManager.Activateand those I call the "activation" constructors. Maybe I should change the name. - Yes, JCWs already differ significantly based on
$(_TypeMapKind): "mvid" (for marshal methods) and "strings-asm" (for runtime native members registartion using reflection and dynamic codegen). Currently, I don't expect the JCW to contain any static constructor and instead lazily get function pointers similarly to marshal methods, but that could change if we go in this direction instead: [Draft][Proposal] Trimmable Type Map #10757 (comment)
|
|
||
| ### 9.3 IgnoresAccessChecksTo for Protected Constructors | ||
|
|
||
| When the activation constructor is protected or in a base class, the TypeMaps assembly uses `IgnoresAccessChecksToAttribute` to bypass access checks: |
There was a problem hiding this comment.
When did tihs type appear?! (Actually, I can't find this type. Does it exist yet?)
This could possibly address the questions and concerns around referencing non-public types!
There was a problem hiding this comment.
This is a secret feature of the .NET runtime 😄 We need to declare the attribute ourselves, it's not part of the runtime libraries. Some (unofficial) explanation/example is for example here: https://www.strathweb.com/2018/10/no-internalvisibleto-no-problem-bypassing-c-visibility-rules-with-roslyn/
|
|
||
| --- | ||
|
|
||
| ## 10. Java Constructor Generation |
There was a problem hiding this comment.
Suggest renaming to Java Callable Wrapper Constructor Generation
| [UnmanagedCallersOnly] | ||
| public static void n_{MethodName}_mm_{Index}(IntPtr jnienv, IntPtr obj, ...) | ||
| { | ||
| AndroidRuntimeInternal.WaitForBridgeProcessing(); |
There was a problem hiding this comment.
This should follow the pattern from dotnet/java-interop@356485e
[UnmanagedCallersOnly]
public static void n_{MethodName}_mm_{Index}(IntPtr jnienv, IntPtr obj, ...)
{
if (!JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r))
return;
try {
TargetType.n_{MethodName}_{JniSignature}(jnienv, obj, ...);
} catch (Exception __e) {
__r?.OnUserUnhandledException (ref __envp, __e);
} finally {
JniEnvironment.EndMarshalMethod (ref __envp);
}
}as this pattern behaves properly when a Debugger is attached.
Additionally, if we can detect that TargetType.n_{MethodName}_{JniSignature} was emitted by a recent generator (TargetFramework .NET 10+), then no wrapper is needed; it can be called directly.
There was a problem hiding this comment.
Thanks for pointing this out.
| @class_name = internal constant [...] c"c\00o\00m\00/\00...\00[\001\00]\00" | ||
| ``` | ||
|
|
||
| The bracket characters `[` and `]` cannot appear in valid JNI names, guaranteeing no collisions. |
There was a problem hiding this comment.
. is also not valid in JNI names, so if you want a shorter form, you could use .1 instead of [1].
| │ | ||
| ├──────────────────────────────────────────────┐ | ||
| ▼ ▼ | ||
| 3. LLVM Compilation 4. Java Compilation |
There was a problem hiding this comment.
Unfortunate aspect of this is that LLVM generation, compilation, and linking now becomes part of the Debug build process. This will almost certainly slow things down.
Are we sure we want to do that?
There was a problem hiding this comment.
You're right, this will definitely make the first Debug build slower. If we get caching right, it shouldn't have much impact on subsequent builds, until a new type is added to the typemap and the build caches are invalidated. If we went with #10757 (comment), we wouldn't need any LLVM code and we would only be dealing with the .java->.class->.dex steps which we already do in the current Debug builds.
| ├────────────────────┬───────────────────────────┤ │ | ||
| ▼ ▼ ▼ │ | ||
| 6. LLVM Compilation 7. Native Linking 8. R8 (Java shrink) │ | ||
| - .ll → .o - Link ONLY surviving - Uses ProGuard │ |
There was a problem hiding this comment.
I'm concerned about (8). IIRC we generate a ProGuard file to preserve generated Java Callable Wrappers, which means they'll be out of sync with a post-trimmed world.
Maybe we can emit a ProGuard file as part of (5) to remove the trimmed Java types?
There was a problem hiding this comment.
Ah, I see that this is mentioned in §15.5 (3).
There was a problem hiding this comment.
The order in which things are introduced in this document obviously needs improving 😄
| ▼ | ||
| Post-Trimming Filter | ||
| │ | ||
| ├─► Surviving .o files → link into libmarshal_methods.so |
There was a problem hiding this comment.
In Native AOT builds, libmarshal_methods.so shouldn't exist; the contents should be merged into "libApp.so".
| } | ||
|
|
||
| public static void n_OnCreate(IntPtr jnienv, IntPtr native__this, IntPtr bundle) { | ||
| var __this = (MainActivity)Java.Lang.Object.GetObject<MainActivity>(native__this); |
There was a problem hiding this comment.
Is this an "inlined" example, or how you actually want things to work.
I think this should actually be:
public static void n_OnCreate(IntPtr jnienv, IntPtr native__this, IntPtr bundle)
=> Activity.n_OnCreate_Landroid_os_Bundle_Landroid_os_PersistableBundle_(jnienv, native__this, bundle);If it's not an inlined example, then the codegen pattern needs to follow that of dotnet/java-interop@356485e so that exceptions are properly marshaled.
The one scenario where the *_Proxy methods should contain the "real" method invocation is for [Export] methods, as there is no generated marshal method to reference.
There was a problem hiding this comment.
this is a simplified ("inlined") example. I will consider replacing it with ... to make it more obvious
|
|
||
| **Why this works:** | ||
| 1. unconditional TypeMapAttribute **unconditionally preserves** MainActivity_Proxy | ||
| 2. MainActivity_Proxy has **direct method references** to MainActivity |
There was a problem hiding this comment.
I'm somewhat suspicious of this, because it means we can't "delegate" to the generator-emitted marshal method. This will result in code bloat, as (otherwise identical) methods can't be shared across subclasses.
There was a problem hiding this comment.
This will result in code bloat, as (otherwise identical) methods can't be shared across subclasses
Let me try to show this on an example to see if I understand your point:
class A : Activity { public override void OnCreate(...) { ... } }
class B : A { public override void OnCreate(...) { ... } }Now the generated code of the ACW proxy types will look something like this:
class A_Proxy {
[UnmanagedCallersOnly]
public static void n_OnCreate_mm(...) {
var a = Object.GetObject<A>(thisHandle);
a.n_OnCreate(...);
}
}
class B_Proxy {
[UnmanagedCallersOnly]
public static void n_OnCreate_mm(...) {
var b = Object.GetObject<B>(thisHandle);
b.n_OnCreate(...);
}
}So you're saying that instead of generating B.n_OnCreate_mm, the Java native method could point directly to A.n_OnCreate_mm and save the basically duplicate IL and the .ll trampoline for this virtual method? That's definitely something we should do. Since B : A, we can expect A_Proxy to exist when B exists.
Or did you have something else in mind?
| | Detection Criteria | Preservation | Reason | | ||
| |-------------------|--------------|--------| | ||
| | User type with `[Activity]`, `[Service]`, etc. attribute | **Unconditional** | Android creates these | | ||
| | User type subclassing Android component (Activity, etc.) | **Unconditional** | Android creates these | |
There was a problem hiding this comment.
I'm uncertain what is meant by this. If I have a "random" Activity subclass without [Activity]:
partial class MyOtherwiseUnreferencedActivity : Activity {
}then I should want that type to be trimmed away. Android cannot access that Activity unless it's in AndroidManifest.xml.
…though as a complication, AndroidManifest.xml can contain user-written XML fragments, so it is possible to have an <activity/> element that references MyOtherwiseUnreferencedActivity without use of [Register] or [Activity]. This can be determined by reading AndroidManifest.xml.
There was a problem hiding this comment.
This section is maybe overly ambitious at writing exactly what needs to be preserved 😄 The source for this information is the existing implementation in the "java stub generator" and "jcw generator" and friends and also the MarkJavaObjects + other custom trimmer steps.
I will revisit this section and I'll adapt it based on your input. The existing implementation already scans layout xml files and mark types manually. I need to doublecheck if we scan AndroidManifest.xml or not. Apparently we should. The unconditional preservation of activities is based on the assumption "if you declare an activity in your app, you probably also want to use it, otherwise you would delete it, right?". we can do better.
| | User type with `[Activity]`, `[Service]`, etc. attribute | **Unconditional** | Android creates these | | ||
| | User type subclassing Android component (Activity, etc.) | **Unconditional** | Android creates these | | ||
| | Custom view referenced in layout XML | **Unconditional** | Android inflates these | | ||
| | Interface with `[Register]` | **Trimmable** | Only if .NET implements/uses | |
There was a problem hiding this comment.
Please clarify "Reason" entry. Is the interface trimmable if .NET implements it? (Which sounds backwards?) Or is the interface preserved if a .NET type implements it?
Closely related question: what about interface members. IIRC the trimmer could remove methods/properties/etc. that it determined weren't used by the app. In the case of bound Java interfaces, that would be Bad™.
| ``` | ||
|
|
||
| **LLVM IR Generation:** For Native AOT, we generate LLVM IR files (`.ll`) that define the JNI entry points. These are compiled alongside the ILC output and linked into the final shared library. This approach: | ||
| - Avoids Java source generation for JCW method stubs |
There was a problem hiding this comment.
I don't understand what you mean by this. .java files (and eventually .class and .dex files) are need by the Android run time in order to invoke the Java native method. The .java files exist to declare the native method!
How do you avoid "Java source generation"?
There was a problem hiding this comment.
This section clearly doesn't make sense. I'm not sure why I did not catch this earlier. Sorry for the confusion.
| // Array creation (AOT-safe) | ||
| // rank=1 for T[], rank=2 for T[][] | ||
| Array CreateArray(Type elementType, int length, int rank); |
There was a problem hiding this comment.
I believe this is outdated and it is actually not needed (replaced by JavaPeerContainerFactory). I need to double check.
| // Array creation (AOT-safe) | |
| // rank=1 for T[], rank=2 for T[][] | |
| Array CreateArray(Type elementType, int length, int rank); |
@agocke I'm not sure if I understand what you meant exactly. do you mean we could avoid generating any .ll code and use JNI's runtime native method registration mechanism? using Java.Interop;
[A_Proxy]
unsafe class A_Proxy : JavaPeerProxy
{
// Fully replaces GetFunctionPointer
public override void RegisterNatives(JniType javaClass)
{
var args = stackalloc JniNativeMethodRegistration[1];
args[0] = new JniNativeMethodRegistration("x"u8, "()V"u8, (IntPtr)(delegate*<..., void>)&X); // note: we don't have ReadOnlySpan<byte> overloads of JniNativeMethodRegistration yet in dotnet/java-interop, but we could easily add it
type.RegisterNativeMethods(args);
}
[UnmanagedCallersOnly]
private static void X(...) { ... }
}Previously, I wanted to base the code on the marshal methods UCO + .ll trampolines + java |
This specification defines the architecture for enabling Java-to-.NET interoperability in .NET Android applications using the .NET Type Mapping API. The design is fully compatible with Native AOT and trimming.
Expands on dotnet/runtime#120121
Proof of concept
See https://github.com/dotnet/android/compare/dev/simonrozsival/trimmable-typemap
Core idea
Each Java peer type is registered using assembly-level attributes:
The
*_Proxytypes are generated attribute classes which is applied to itself:Why this works:
GetCustomAttribute<T>()instantiates attributes in an trimming and AOT-safe mannertrimTargetparameter ensures the mapping is preserved when the target type survives trimming/cc @jtschuster
Related