Skip to content

Commit 8e01ea4

Browse files
[TrimmableTypeMap] Add JCW Java source generator
Add JcwJavaSourceGenerator that produces Java Callable Wrapper .java source files from scanned JavaPeerInfo records: - Package declarations, class declarations with extends/implements - Static initializer with registerNatives call - Constructors with super() delegation and activation guards - Method overrides delegating to native callbacks - Native method declarations for JNI registration - Uses raw string literals (C# $$""") for readable Java templates - Comprehensive test coverage for all generation scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e2a6a83 commit 8e01ea4

File tree

2 files changed

+694
-0
lines changed

2 files changed

+694
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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+
sealed class JcwJavaSourceGenerator
12+
{
13+
/// <summary>
14+
/// Generates .java source files for all ACW types and writes them to the output directory.
15+
/// Returns the list of generated file paths.
16+
/// </summary>
17+
public IReadOnlyList<string> Generate (IReadOnlyList<JavaPeerInfo> types, string outputDirectory)
18+
{
19+
if (types is null) {
20+
throw new ArgumentNullException (nameof (types));
21+
}
22+
if (outputDirectory is null) {
23+
throw new ArgumentNullException (nameof (outputDirectory));
24+
}
25+
26+
var generatedFiles = new List<string> ();
27+
28+
foreach (var type in types) {
29+
if (type.DoNotGenerateAcw || type.IsInterface) {
30+
continue;
31+
}
32+
33+
string filePath = GetOutputFilePath (type, outputDirectory);
34+
string? dir = Path.GetDirectoryName (filePath);
35+
if (dir != null) {
36+
Directory.CreateDirectory (dir);
37+
}
38+
39+
using var writer = new StreamWriter (filePath);
40+
Generate (type, writer);
41+
generatedFiles.Add (filePath);
42+
}
43+
44+
return generatedFiles;
45+
}
46+
47+
/// <summary>
48+
/// Generates a single .java source file for the given type.
49+
/// </summary>
50+
internal void Generate (JavaPeerInfo type, TextWriter writer)
51+
{
52+
WritePackageDeclaration (type, writer);
53+
WriteClassDeclaration (type, writer);
54+
WriteStaticInitializer (type, writer);
55+
WriteConstructors (type, writer);
56+
WriteMethods (type, writer);
57+
WriteClassClose (writer);
58+
}
59+
60+
static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
61+
{
62+
// JNI name uses '/' as separator and '$' for nested types
63+
// e.g., "com/example/MainActivity" → "com/example/MainActivity.java"
64+
// Nested types: "com/example/Outer$Inner" → "com/example/Outer$Inner.java" (same file convention)
65+
string relativePath = type.JavaName + ".java";
66+
return Path.Combine (outputDirectory, relativePath);
67+
}
68+
69+
static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer)
70+
{
71+
string? package = GetJavaPackageName (type.JavaName);
72+
if (package != null) {
73+
writer.Write ("package ");
74+
writer.Write (package);
75+
writer.WriteLine (';');
76+
writer.WriteLine ();
77+
}
78+
}
79+
80+
static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer)
81+
{
82+
string abstractModifier = type.IsAbstract && !type.IsInterface ? "abstract " : "";
83+
string className = GetJavaSimpleName (type.JavaName);
84+
85+
writer.Write ($"public {abstractModifier}class {className}\n");
86+
87+
// extends clause
88+
if (type.BaseJavaName != null) {
89+
writer.WriteLine ($"\textends {JniNameToJavaName (type.BaseJavaName)}");
90+
}
91+
92+
// implements clause — always includes IGCUserPeer, plus any implemented interfaces
93+
writer.Write ("\timplements\n\t\tmono.android.IGCUserPeer");
94+
95+
foreach (var iface in type.ImplementedInterfaceJavaNames) {
96+
writer.Write ($",\n\t\t{JniNameToJavaName (iface)}");
97+
}
98+
99+
writer.WriteLine ();
100+
writer.WriteLine ('{');
101+
}
102+
103+
static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer)
104+
{
105+
string className = GetJavaSimpleName (type.JavaName);
106+
writer.Write ($$"""
107+
static {
108+
mono.android.Runtime.registerNatives ({{className}}.class);
109+
}
110+
111+
112+
""");
113+
}
114+
115+
static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
116+
{
117+
string simpleClassName = GetJavaSimpleName (type.JavaName);
118+
119+
foreach (var ctor in type.JavaConstructors) {
120+
string parameters = FormatParameterList (ctor.Parameters);
121+
string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctor.Parameters);
122+
string args = FormatArgumentList (ctor.Parameters);
123+
124+
writer.Write ($$"""
125+
public {{simpleClassName}} ({{parameters}})
126+
{
127+
super ({{superArgs}});
128+
if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}});
129+
}
130+
131+
132+
""");
133+
}
134+
135+
// Write native constructor declarations
136+
foreach (var ctor in type.JavaConstructors) {
137+
string parameters = FormatParameterList (ctor.Parameters);
138+
writer.WriteLine ($"\tprivate native void nctor_{ctor.ConstructorIndex} ({parameters});");
139+
}
140+
141+
if (type.JavaConstructors.Count > 0) {
142+
writer.WriteLine ();
143+
}
144+
}
145+
146+
static void WriteMethods (JavaPeerInfo type, TextWriter writer)
147+
{
148+
foreach (var method in type.MarshalMethods) {
149+
if (method.IsConstructor) {
150+
continue;
151+
}
152+
153+
string javaReturnType = JniTypeToJava (method.JniReturnType);
154+
bool isVoid = method.JniReturnType == "V";
155+
string parameters = FormatParameterList (method.Parameters);
156+
string args = FormatArgumentList (method.Parameters);
157+
string returnPrefix = isVoid ? "" : "return ";
158+
159+
// throws clause for [Export] methods
160+
string throwsClause = "";
161+
if (method.ThrownNames != null && method.ThrownNames.Count > 0) {
162+
throwsClause = $"\n\t\tthrows {string.Join (", ", method.ThrownNames)}";
163+
}
164+
165+
if (method.Connector != null) {
166+
writer.Write ($$"""
167+
168+
@Override
169+
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
170+
{
171+
{{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
172+
}
173+
public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
174+
175+
""");
176+
} else {
177+
writer.Write ($$"""
178+
179+
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
180+
{
181+
{{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
182+
}
183+
public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
184+
185+
""");
186+
}
187+
}
188+
}
189+
190+
static void WriteClassClose (TextWriter writer)
191+
{
192+
writer.WriteLine ('}');
193+
}
194+
195+
static string FormatParameterList (IReadOnlyList<JniParameterInfo> parameters)
196+
{
197+
if (parameters.Count == 0) {
198+
return "";
199+
}
200+
201+
var sb = new System.Text.StringBuilder ();
202+
for (int i = 0; i < parameters.Count; i++) {
203+
if (i > 0) {
204+
sb.Append (", ");
205+
}
206+
sb.Append (JniTypeToJava (parameters [i].JniType));
207+
sb.Append (" p");
208+
sb.Append (i);
209+
}
210+
return sb.ToString ();
211+
}
212+
213+
static string FormatArgumentList (IReadOnlyList<JniParameterInfo> parameters)
214+
{
215+
if (parameters.Count == 0) {
216+
return "";
217+
}
218+
219+
var sb = new System.Text.StringBuilder ();
220+
for (int i = 0; i < parameters.Count; i++) {
221+
if (i > 0) {
222+
sb.Append (", ");
223+
}
224+
sb.Append ('p');
225+
sb.Append (i);
226+
}
227+
return sb.ToString ();
228+
}
229+
230+
/// <summary>
231+
/// Converts a JNI type name to a Java source type name.
232+
/// e.g., "android/app/Activity" → "android.app.Activity"
233+
/// </summary>
234+
internal static string JniNameToJavaName (string jniName)
235+
{
236+
return jniName.Replace ('/', '.');
237+
}
238+
239+
/// <summary>
240+
/// Extracts the Java package name from a JNI type name.
241+
/// e.g., "com/example/MainActivity" → "com.example"
242+
/// Returns null for types without a package.
243+
/// </summary>
244+
internal static string? GetJavaPackageName (string jniName)
245+
{
246+
int lastSlash = jniName.LastIndexOf ('/');
247+
if (lastSlash < 0) {
248+
return null;
249+
}
250+
return jniName.Substring (0, lastSlash).Replace ('/', '.');
251+
}
252+
253+
/// <summary>
254+
/// Extracts the simple Java class name from a JNI type name.
255+
/// e.g., "com/example/MainActivity" → "MainActivity"
256+
/// e.g., "com/example/Outer$Inner" → "Outer$Inner" (preserves nesting separator)
257+
/// </summary>
258+
internal static string GetJavaSimpleName (string jniName)
259+
{
260+
int lastSlash = jniName.LastIndexOf ('/');
261+
return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName;
262+
}
263+
264+
/// <summary>
265+
/// Converts a JNI type descriptor to a Java source type.
266+
/// e.g., "V" → "void", "I" → "int", "Landroid/os/Bundle;" → "android.os.Bundle"
267+
/// </summary>
268+
internal static string JniTypeToJava (string jniType)
269+
{
270+
if (jniType.Length == 1) {
271+
return jniType [0] switch {
272+
'V' => "void",
273+
'Z' => "boolean",
274+
'B' => "byte",
275+
'C' => "char",
276+
'S' => "short",
277+
'I' => "int",
278+
'J' => "long",
279+
'F' => "float",
280+
'D' => "double",
281+
_ => throw new ArgumentException ($"Unknown JNI primitive type: {jniType}"),
282+
};
283+
}
284+
285+
// Array types: "[I" → "int[]", "[Ljava/lang/String;" → "java.lang.String[]"
286+
if (jniType [0] == '[') {
287+
return JniTypeToJava (jniType.Substring (1)) + "[]";
288+
}
289+
290+
// Object types: "Landroid/os/Bundle;" → "android.os.Bundle"
291+
if (jniType [0] == 'L' && jniType [jniType.Length - 1] == ';') {
292+
return JniNameToJavaName (jniType.Substring (1, jniType.Length - 2));
293+
}
294+
295+
throw new ArgumentException ($"Unknown JNI type descriptor: {jniType}");
296+
}
297+
}

0 commit comments

Comments
 (0)