Skip to content

Commit f158bed

Browse files
[TrimmableTypeMap] Add UCO wrappers, JCW generator, and JNI signature helpers (#10917)
Fixes: #10799 ## Summary Adds UCO (UnifiedCallableObject) wrapper generation, JCW (Java Callable Wrapper) Java source generation, and JNI signature parsing utilities on top of the TypeMap proxy groundwork from PR #10808. ### Generator additions - **JcwJavaSourceGenerator** — generates `.java` source files for each ACW type with `registerNatives` in `static {}` blocks, `native` method declarations, constructor chains with `super()` calls, and `@Override` annotations - **JniSignatureHelper** — JNI signature parsing (parameter types, return types, JNI↔Java type conversion, CLR type encoding for IL emission) - **ModelBuilder** — extended with UCO method/constructor building, native registration data, and `isAcw` classification - **TypeMapAssemblyEmitter** — extended with `EmitUcoMethod` (UnmanagedCallersOnly wrappers that delegate to n_* callbacks), `EmitUcoConstructor`, `EmitRegisterNatives` (JNI native method registration), and `IAndroidCallableWrapper` interface implementation for ACW proxy types ### Scanner enrichment - **JavaPeerInfo** — added `BaseJavaName`, `ImplementedInterfaceJavaNames`, `JavaConstructors`, `JniParameterInfo`, `ThrownNames` for downstream generators - **JavaPeerScanner** — resolver methods for base types, interfaces, constructors; fixed `NativeCallbackName` for constructors (`n_ctor` instead of invalid `n_.ctor`) ### Model additions - **UcoMethodData** / **UcoConstructorData** — describe UnmanagedCallersOnly wrappers to emit - **NativeRegistrationData** — JNI native method registrations for RegisterNatives ### Tests - 215 tests covering all generators, model builder scenarios, scanner behavior, and edge cases - Test fixtures extended with `ExportWithThrows` type - Stream-based test helpers avoid disk I/O for faster, more reliable tests ## Follow-up work - **`GetContainerFactory()`** — moved to #10791 (runtime issue); will emit the method body once `JavaPeerContainerFactory` is defined - **MSBuild task integration** (`_GenerateJavaStubs` decomposition) — tracked in #10807 - **Implementor/EventDispatcher trimming optimization** — tracked in #10911 ## Note on `GetFunctionPointer` The issue spec mentioned `GetFunctionPointer(int)` as an alternative dispatch mechanism. Since we use `RegisterNatives` (which binds JNI native methods to UCO wrappers at class load time), `GetFunctionPointer` is not needed.
1 parent 52c36b4 commit f158bed

14 files changed

Lines changed: 1601 additions & 24 deletions

File tree

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
5+
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
6+
7+
/// <summary>
8+
/// Generates JCW (Java Callable Wrapper) .java source files from scanned <see cref="JavaPeerInfo"/> records.
9+
/// Only processes ACW types (where <see cref="JavaPeerInfo.DoNotGenerateAcw"/> is false).
10+
/// </summary>
11+
/// <remarks>
12+
/// <para>Each generated .java file looks like this (pseudo-Java):</para>
13+
/// <code>
14+
/// package com.example;
15+
///
16+
/// public class MainActivity
17+
/// extends android.app.Activity
18+
/// implements
19+
/// mono.android.IGCUserPeer,
20+
/// android.view.View.OnClickListener
21+
/// {
22+
/// static {
23+
/// mono.android.Runtime.registerNatives (MainActivity.class);
24+
/// }
25+
///
26+
/// public MainActivity (android.content.Context p0)
27+
/// {
28+
/// super (p0);
29+
/// if (getClass () == MainActivity.class) nctor_0 (p0);
30+
/// }
31+
/// private native void nctor_0 (android.content.Context p0);
32+
///
33+
/// @Override
34+
/// public void onCreate (android.os.Bundle p0)
35+
/// {
36+
/// n_onCreate (p0);
37+
/// }
38+
/// public native void n_onCreate (android.os.Bundle p0);
39+
/// }
40+
/// </code>
41+
/// </remarks>
42+
sealed class JcwJavaSourceGenerator
43+
{
44+
/// <summary>
45+
/// Generates .java source files for all ACW types and writes them to the output directory.
46+
/// Returns the list of generated file paths.
47+
/// </summary>
48+
public IReadOnlyList<string> Generate (IReadOnlyList<JavaPeerInfo> types, string outputDirectory)
49+
{
50+
if (types is null) {
51+
throw new ArgumentNullException (nameof (types));
52+
}
53+
if (outputDirectory is null) {
54+
throw new ArgumentNullException (nameof (outputDirectory));
55+
}
56+
57+
var generatedFiles = new List<string> ();
58+
59+
foreach (var type in types) {
60+
if (type.DoNotGenerateAcw || type.IsInterface) {
61+
continue;
62+
}
63+
64+
string filePath = GetOutputFilePath (type, outputDirectory);
65+
string? dir = Path.GetDirectoryName (filePath);
66+
if (dir != null) {
67+
Directory.CreateDirectory (dir);
68+
}
69+
70+
using var writer = new StreamWriter (filePath);
71+
Generate (type, writer);
72+
generatedFiles.Add (filePath);
73+
}
74+
75+
return generatedFiles;
76+
}
77+
78+
/// <summary>
79+
/// Generates a single .java source file for the given type.
80+
/// </summary>
81+
internal void Generate (JavaPeerInfo type, TextWriter writer)
82+
{
83+
writer.NewLine = "\n";
84+
WritePackageDeclaration (type, writer);
85+
WriteClassDeclaration (type, writer);
86+
WriteStaticInitializer (type, writer);
87+
WriteConstructors (type, writer);
88+
WriteMethods (type, writer);
89+
WriteGCUserPeerMethods (writer);
90+
WriteClassClose (writer);
91+
}
92+
93+
static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
94+
{
95+
JniSignatureHelper.ValidateJniName (type.JavaName);
96+
string relativePath = type.JavaName + ".java";
97+
return Path.Combine (outputDirectory, relativePath);
98+
}
99+
100+
/// <summary>
101+
/// Validates that the JNI name is well-formed: non-empty, each segment separated by '/'
102+
/// contains only valid Java identifier characters (letters, digits, '_', '$').
103+
/// This also prevents path traversal (e.g., ".." segments, rooted paths, backslashes).
104+
/// </summary>
105+
static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer)
106+
{
107+
string? package = JniSignatureHelper.GetJavaPackageName (type.JavaName);
108+
if (package != null) {
109+
writer.Write ("package ");
110+
writer.Write (package);
111+
writer.WriteLine (';');
112+
writer.WriteLine ();
113+
}
114+
}
115+
116+
static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer)
117+
{
118+
string abstractModifier = type.IsAbstract && !type.IsInterface ? "abstract " : "";
119+
string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
120+
121+
writer.Write ($"public {abstractModifier}class {className}\n");
122+
123+
// extends clause
124+
if (type.BaseJavaName != null) {
125+
writer.WriteLine ($"\textends {JniSignatureHelper.JniNameToJavaName (type.BaseJavaName)}");
126+
}
127+
128+
// implements clause — always includes IGCUserPeer, plus any implemented interfaces
129+
writer.Write ("\timplements\n\t\tmono.android.IGCUserPeer");
130+
131+
foreach (var iface in type.ImplementedInterfaceJavaNames) {
132+
writer.Write ($",\n\t\t{JniSignatureHelper.JniNameToJavaName (iface)}");
133+
}
134+
135+
writer.WriteLine ();
136+
writer.WriteLine ('{');
137+
}
138+
139+
static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer)
140+
{
141+
string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
142+
writer.Write ($$"""
143+
static {
144+
mono.android.Runtime.registerNatives ({{className}}.class);
145+
}
146+
147+
148+
""");
149+
}
150+
151+
static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
152+
{
153+
string simpleClassName = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
154+
155+
foreach (var ctor in type.JavaConstructors) {
156+
var ctorParams = JniSignatureHelper.ParseParameters (ctor.JniSignature);
157+
string parameters = FormatParameterList (ctorParams);
158+
string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctorParams);
159+
string args = FormatArgumentList (ctorParams);
160+
161+
writer.Write ($$"""
162+
public {{simpleClassName}} ({{parameters}})
163+
{
164+
super ({{superArgs}});
165+
if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}});
166+
}
167+
168+
169+
""");
170+
}
171+
172+
// Write native constructor declarations
173+
foreach (var ctor in type.JavaConstructors) {
174+
var nativeCtorParams = JniSignatureHelper.ParseParameters (ctor.JniSignature);
175+
string parameters = FormatParameterList (nativeCtorParams);
176+
writer.WriteLine ($"\tprivate native void nctor_{ctor.ConstructorIndex} ({parameters});");
177+
}
178+
179+
if (type.JavaConstructors.Count > 0) {
180+
writer.WriteLine ();
181+
}
182+
}
183+
184+
static void WriteMethods (JavaPeerInfo type, TextWriter writer)
185+
{
186+
foreach (var method in type.MarshalMethods) {
187+
if (method.IsConstructor) {
188+
continue;
189+
}
190+
191+
string jniReturnType = JniSignatureHelper.ParseReturnTypeString (method.JniSignature);
192+
string javaReturnType = JniSignatureHelper.JniTypeToJava (jniReturnType);
193+
bool isVoid = jniReturnType == "V";
194+
var methodParams = JniSignatureHelper.ParseParameters (method.JniSignature);
195+
string parameters = FormatParameterList (methodParams);
196+
string args = FormatArgumentList (methodParams);
197+
string returnPrefix = isVoid ? "" : "return ";
198+
199+
// throws clause for [Export] methods
200+
string throwsClause = "";
201+
if (method.ThrownNames != null && method.ThrownNames.Count > 0) {
202+
throwsClause = $"\n\t\tthrows {string.Join (", ", method.ThrownNames)}";
203+
}
204+
205+
if (method.Connector != null) {
206+
writer.Write ($$"""
207+
208+
@Override
209+
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
210+
{
211+
{{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
212+
}
213+
public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
214+
215+
""");
216+
} else {
217+
writer.Write ($$"""
218+
219+
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
220+
{
221+
{{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
222+
}
223+
public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
224+
225+
""");
226+
}
227+
}
228+
}
229+
230+
static void WriteGCUserPeerMethods (TextWriter writer)
231+
{
232+
writer.Write ("""
233+
234+
private java.util.ArrayList refList;
235+
public void monodroidAddReference (java.lang.Object obj)
236+
{
237+
if (refList == null)
238+
refList = new java.util.ArrayList ();
239+
refList.add (obj);
240+
}
241+
242+
public void monodroidClearReferences ()
243+
{
244+
if (refList != null)
245+
refList.clear ();
246+
}
247+
248+
""");
249+
}
250+
251+
static void WriteClassClose (TextWriter writer)
252+
{
253+
writer.WriteLine ('}');
254+
}
255+
256+
static string FormatParameterList (IReadOnlyList<JniParameterInfo> parameters)
257+
{
258+
if (parameters.Count == 0) {
259+
return "";
260+
}
261+
262+
var sb = new System.Text.StringBuilder ();
263+
for (int i = 0; i < parameters.Count; i++) {
264+
if (i > 0) {
265+
sb.Append (", ");
266+
}
267+
sb.Append (JniSignatureHelper.JniTypeToJava (parameters [i].JniType));
268+
sb.Append (" p");
269+
sb.Append (i);
270+
}
271+
return sb.ToString ();
272+
}
273+
274+
static string FormatArgumentList (IReadOnlyList<JniParameterInfo> parameters)
275+
{
276+
if (parameters.Count == 0) {
277+
return "";
278+
}
279+
280+
var sb = new System.Text.StringBuilder ();
281+
for (int i = 0; i < parameters.Count; i++) {
282+
if (i > 0) {
283+
sb.Append (", ");
284+
}
285+
sb.Append ('p');
286+
sb.Append (i);
287+
}
288+
return sb.ToString ();
289+
}
290+
291+
}

0 commit comments

Comments
 (0)