Skip to content

Commit c016cbe

Browse files
[TrimmableTypeMap] Add TypeMap assembly generators with model builder
Add the pipeline that transforms scanned JavaPeerInfo records into TypeMap .dll assemblies: - Scanner enrichment: new fields on JavaPeerInfo/MarshalMethodInfo for UCO generation (DeclaringTypeName, NativeCallbackName, Parameters, JniReturnType, JavaConstructors, ManagedTypeNamespace/ShortName) - JniSignatureHelper: JNI signature parsing for parameter/return types - TypeMapAssemblyData model: records for proxy types, UCO wrappers, native registrations, activation constructors, type map associations - ModelBuilder: transforms JavaPeerInfo into TypeMapAssemblyData with alias detection, ACW filtering, proxy naming, cross-assembly support - TypeMapAssemblyEmitter: PE/IL emission using System.Reflection.Metadata - TypeMapAssemblyGenerator: orchestrates Build + Emit pipeline - RootTypeMapAssemblyGenerator: generates root _Microsoft.Android.TypeMaps.dll - Comprehensive test coverage for model builder, assembly generator, and root assembly generator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 03926b5 commit c016cbe

File tree

11 files changed

+4399
-0
lines changed

11 files changed

+4399
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection.Metadata;
4+
using System.Reflection.Metadata.Ecma335;
5+
6+
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
7+
8+
/// <summary>JNI primitive type kinds used for mapping JNI signatures → CLR types.</summary>
9+
enum JniParamKind
10+
{
11+
Void, // V
12+
Boolean, // Z → sbyte
13+
Byte, // B → sbyte
14+
Char, // C → char
15+
Short, // S → short
16+
Int, // I → int
17+
Long, // J → long
18+
Float, // F → float
19+
Double, // D → double
20+
Object, // L...; or [ → IntPtr
21+
}
22+
23+
/// <summary>Helpers for parsing JNI method signatures.</summary>
24+
static class JniSignatureHelper
25+
{
26+
/// <summary>Parses the parameter types from a JNI method signature like "(Landroid/os/Bundle;)V".</summary>
27+
public static List<JniParamKind> ParseParameterTypes (string jniSignature)
28+
{
29+
var result = new List<JniParamKind> ();
30+
int i = 1; // skip opening '('
31+
while (i < jniSignature.Length && jniSignature [i] != ')') {
32+
result.Add (ParseSingleType (jniSignature, ref i));
33+
}
34+
return result;
35+
}
36+
37+
/// <summary>Parses the raw JNI type descriptor strings from a JNI method signature.</summary>
38+
public static List<string> ParseParameterTypeStrings (string jniSignature)
39+
{
40+
var result = new List<string> ();
41+
int i = 1; // skip opening '('
42+
while (i < jniSignature.Length && jniSignature [i] != ')') {
43+
int start = i;
44+
SkipSingleType (jniSignature, ref i);
45+
result.Add (jniSignature.Substring (start, i - start));
46+
}
47+
return result;
48+
}
49+
50+
/// <summary>Extracts the return type descriptor from a JNI method signature.</summary>
51+
public static string ParseReturnTypeString (string jniSignature)
52+
{
53+
int i = jniSignature.IndexOf (')') + 1;
54+
return jniSignature.Substring (i);
55+
}
56+
57+
/// <summary>Parses the return type from a JNI method signature.</summary>
58+
public static JniParamKind ParseReturnType (string jniSignature)
59+
{
60+
int i = jniSignature.IndexOf (')') + 1;
61+
return ParseSingleType (jniSignature, ref i);
62+
}
63+
64+
static JniParamKind ParseSingleType (string sig, ref int i)
65+
{
66+
switch (sig [i]) {
67+
case 'V': i++; return JniParamKind.Void;
68+
case 'Z': i++; return JniParamKind.Boolean;
69+
case 'B': i++; return JniParamKind.Byte;
70+
case 'C': i++; return JniParamKind.Char;
71+
case 'S': i++; return JniParamKind.Short;
72+
case 'I': i++; return JniParamKind.Int;
73+
case 'J': i++; return JniParamKind.Long;
74+
case 'F': i++; return JniParamKind.Float;
75+
case 'D': i++; return JniParamKind.Double;
76+
case 'L':
77+
i = sig.IndexOf (';', i) + 1;
78+
return JniParamKind.Object;
79+
case '[':
80+
i++;
81+
ParseSingleType (sig, ref i); // skip element type
82+
return JniParamKind.Object;
83+
default:
84+
throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}");
85+
}
86+
}
87+
88+
static void SkipSingleType (string sig, ref int i)
89+
{
90+
switch (sig [i]) {
91+
case 'V': case 'Z': case 'B': case 'C': case 'S':
92+
case 'I': case 'J': case 'F': case 'D':
93+
i++;
94+
break;
95+
case 'L':
96+
i = sig.IndexOf (';', i) + 1;
97+
break;
98+
case '[':
99+
i++;
100+
SkipSingleType (sig, ref i);
101+
break;
102+
default:
103+
throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}");
104+
}
105+
}
106+
107+
/// <summary>Encodes the CLR type for a JNI parameter kind into a signature type encoder.</summary>
108+
public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind)
109+
{
110+
switch (kind) {
111+
case JniParamKind.Boolean: encoder.SByte (); break;
112+
case JniParamKind.Byte: encoder.SByte (); break;
113+
case JniParamKind.Char: encoder.Char (); break;
114+
case JniParamKind.Short: encoder.Int16 (); break;
115+
case JniParamKind.Int: encoder.Int32 (); break;
116+
case JniParamKind.Long: encoder.Int64 (); break;
117+
case JniParamKind.Float: encoder.Single (); break;
118+
case JniParamKind.Double: encoder.Double (); break;
119+
case JniParamKind.Object: encoder.IntPtr (); break;
120+
default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type");
121+
}
122+
}
123+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
5+
6+
/// <summary>
7+
/// Data model for a single TypeMap output assembly.
8+
/// Describes what to emit — the emitter writes this directly into a PE assembly.
9+
/// Built by <see cref="ModelBuilder"/>, consumed by <see cref="TypeMapAssemblyGenerator"/>.
10+
/// </summary>
11+
sealed class TypeMapAssemblyData
12+
{
13+
/// <summary>Assembly name (e.g., "_MyApp.TypeMap").</summary>
14+
public required string AssemblyName { get; init; }
15+
16+
/// <summary>Module file name (e.g., "_MyApp.TypeMap.dll").</summary>
17+
public required string ModuleName { get; init; }
18+
19+
/// <summary>TypeMap entries — one per unique JNI name.</summary>
20+
public List<TypeMapAttributeData> Entries { get; } = new ();
21+
22+
/// <summary>Proxy types to emit in the assembly.</summary>
23+
public List<JavaPeerProxyData> ProxyTypes { get; } = new ();
24+
25+
/// <summary>TypeMapAssociation entries for alias groups (multiple managed types → same JNI name).</summary>
26+
public List<TypeMapAssociationData> Associations { get; } = new ();
27+
28+
/// <summary>Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls.</summary>
29+
public List<string> IgnoresAccessChecksTo { get; } = new ();
30+
}
31+
32+
/// <summary>
33+
/// One [assembly: TypeMap("jni/name", typeof(Proxy))] or
34+
/// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry.
35+
///
36+
/// 2-arg (unconditional): proxy is always preserved — used for ACW types and essential runtime types.
37+
/// 3-arg (trimmable): proxy is preserved only if Target type is referenced by the app.
38+
/// </summary>
39+
sealed record TypeMapAttributeData
40+
{
41+
/// <summary>JNI type name, e.g., "android/app/Activity".</summary>
42+
public required string JniName { get; init; }
43+
44+
/// <summary>
45+
/// Assembly-qualified proxy type reference string.
46+
/// Either points to a generated proxy or to the original managed type.
47+
/// </summary>
48+
public required string ProxyTypeReference { get; init; }
49+
50+
/// <summary>
51+
/// Assembly-qualified target type reference for the trimmable (3-arg) variant.
52+
/// Null for unconditional (2-arg) entries.
53+
/// The trimmer preserves the proxy only if this target type is used by the app.
54+
/// </summary>
55+
public string? TargetTypeReference { get; init; }
56+
57+
/// <summary>True for 2-arg unconditional entries (ACW types, essential runtime types).</summary>
58+
public bool IsUnconditional => TargetTypeReference == null;
59+
}
60+
61+
/// <summary>
62+
/// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy).
63+
/// </summary>
64+
sealed class JavaPeerProxyData
65+
{
66+
/// <summary>Simple type name, e.g., "Java_Lang_Object_Proxy".</summary>
67+
public required string TypeName { get; init; }
68+
69+
/// <summary>Namespace for all proxy types.</summary>
70+
public string Namespace { get; init; } = "_TypeMap.Proxies";
71+
72+
/// <summary>Reference to the managed type this proxy wraps (for ldtoken in TargetType property).</summary>
73+
public required TypeRefData TargetType { get; init; }
74+
75+
/// <summary>Reference to the invoker type (for interfaces/abstract types). Null if not applicable.</summary>
76+
public TypeRefData? InvokerType { get; set; }
77+
78+
/// <summary>Whether this proxy has a CreateInstance that can actually create instances.</summary>
79+
public bool HasActivation => ActivationCtor != null || InvokerType != null;
80+
81+
/// <summary>
82+
/// Activation constructor details. Determines how CreateInstance instantiates the managed peer.
83+
/// </summary>
84+
public ActivationCtorData? ActivationCtor { get; set; }
85+
86+
/// <summary>True if this is an open generic type definition. CreateInstance throws NotSupportedException.</summary>
87+
public bool IsGenericDefinition { get; init; }
88+
89+
/// <summary>Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper).</summary>
90+
public bool IsAcw { get; init; }
91+
92+
/// <summary>UCO method wrappers for marshal methods (non-constructor).</summary>
93+
public List<UcoMethodData> UcoMethods { get; } = new ();
94+
95+
/// <summary>UCO constructor wrappers.</summary>
96+
public List<UcoConstructorData> UcoConstructors { get; } = new ();
97+
98+
/// <summary>RegisterNatives registrations (method name, JNI signature, wrapper name).</summary>
99+
public List<NativeRegistrationData> NativeRegistrations { get; } = new ();
100+
}
101+
102+
/// <summary>
103+
/// A cross-assembly type reference (assembly name + full managed type name).
104+
/// </summary>
105+
sealed record TypeRefData
106+
{
107+
/// <summary>Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner".</summary>
108+
public required string ManagedTypeName { get; init; }
109+
110+
/// <summary>Assembly containing the type, e.g., "Mono.Android".</summary>
111+
public required string AssemblyName { get; init; }
112+
}
113+
114+
/// <summary>
115+
/// An [UnmanagedCallersOnly] static wrapper for a marshal method.
116+
/// Body: load all args → call n_* callback → ret.
117+
/// </summary>
118+
sealed record UcoMethodData
119+
{
120+
/// <summary>Name of the generated wrapper method, e.g., "n_onCreate_uco_0".</summary>
121+
public required string WrapperName { get; init; }
122+
123+
/// <summary>Name of the n_* callback to call, e.g., "n_OnCreate".</summary>
124+
public required string CallbackMethodName { get; init; }
125+
126+
/// <summary>Type containing the callback method.</summary>
127+
public required TypeRefData CallbackType { get; init; }
128+
129+
/// <summary>JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types.</summary>
130+
public required string JniSignature { get; init; }
131+
}
132+
133+
/// <summary>
134+
/// An [UnmanagedCallersOnly] static wrapper for a constructor callback.
135+
/// Signature must match the full JNI native method signature (jnienv + self + ctor params)
136+
/// so the ABI is correct when JNI dispatches the call.
137+
/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)).
138+
/// </summary>
139+
sealed record UcoConstructorData
140+
{
141+
/// <summary>Name of the generated wrapper, e.g., "nctor_0_uco".</summary>
142+
public required string WrapperName { get; init; }
143+
144+
/// <summary>Target type to pass to ActivateInstance.</summary>
145+
public required TypeRefData TargetType { get; init; }
146+
147+
/// <summary>JNI constructor signature, e.g., "(Landroid/content/Context;)V". Used for RegisterNatives registration.</summary>
148+
public required string JniSignature { get; init; }
149+
}
150+
151+
/// <summary>
152+
/// One JNI native method registration in RegisterNatives.
153+
/// </summary>
154+
sealed record NativeRegistrationData
155+
{
156+
/// <summary>JNI method name to register, e.g., "n_onCreate" or "nctor_0".</summary>
157+
public required string JniMethodName { get; init; }
158+
159+
/// <summary>JNI method signature, e.g., "(Landroid/os/Bundle;)V".</summary>
160+
public required string JniSignature { get; init; }
161+
162+
/// <summary>Name of the UCO wrapper method whose function pointer to register.</summary>
163+
public required string WrapperMethodName { get; init; }
164+
}
165+
166+
/// <summary>
167+
/// Describes how the proxy's CreateInstance should construct the managed peer.
168+
/// </summary>
169+
sealed record ActivationCtorData
170+
{
171+
/// <summary>Type that declares the activation constructor (may be a base type).</summary>
172+
public required TypeRefData DeclaringType { get; init; }
173+
174+
/// <summary>True when the leaf type itself declares the activation ctor.</summary>
175+
public required bool IsOnLeafType { get; init; }
176+
177+
/// <summary>The style of activation ctor (XamarinAndroid or JavaInterop).</summary>
178+
public required ActivationCtorStyle Style { get; init; }
179+
}
180+
181+
/// <summary>
182+
/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry.
183+
/// Links a managed type to the proxy that holds its alias TypeMap entry.
184+
/// </summary>
185+
sealed record TypeMapAssociationData
186+
{
187+
/// <summary>Assembly-qualified source type reference (the managed alias type).</summary>
188+
public required string SourceTypeReference { get; init; }
189+
190+
/// <summary>Assembly-qualified proxy type reference (the alias holder proxy).</summary>
191+
public required string AliasProxyTypeReference { get; init; }
192+
}

0 commit comments

Comments
 (0)