Skip to content

Commit 4d1c7dc

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 7800276 commit 4d1c7dc

File tree

2 files changed

+713
-0
lines changed

2 files changed

+713
-0
lines changed
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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) {
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+
writer.Write ($$"""
166+
@Override
167+
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
168+
{
169+
{{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
170+
}
171+
public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
172+
173+
""");
174+
}
175+
}
176+
177+
static void WriteClassClose (TextWriter writer)
178+
{
179+
writer.WriteLine ('}');
180+
}
181+
182+
static string FormatParameterList (IReadOnlyList<JniParameterInfo> parameters)
183+
{
184+
if (parameters.Count == 0) {
185+
return "";
186+
}
187+
188+
var sb = new System.Text.StringBuilder ();
189+
for (int i = 0; i < parameters.Count; i++) {
190+
if (i > 0) {
191+
sb.Append (", ");
192+
}
193+
sb.Append (JniTypeToJava (parameters [i].JniType));
194+
sb.Append (" p");
195+
sb.Append (i);
196+
}
197+
return sb.ToString ();
198+
}
199+
200+
static string FormatArgumentList (IReadOnlyList<JniParameterInfo> parameters)
201+
{
202+
if (parameters.Count == 0) {
203+
return "";
204+
}
205+
206+
var sb = new System.Text.StringBuilder ();
207+
for (int i = 0; i < parameters.Count; i++) {
208+
if (i > 0) {
209+
sb.Append (", ");
210+
}
211+
sb.Append ('p');
212+
sb.Append (i);
213+
}
214+
return sb.ToString ();
215+
}
216+
217+
/// <summary>
218+
/// Converts a JNI type name to a Java source type name.
219+
/// e.g., "android/app/Activity" → "android.app.Activity"
220+
/// </summary>
221+
internal static string JniNameToJavaName (string jniName)
222+
{
223+
return jniName.Replace ('/', '.');
224+
}
225+
226+
/// <summary>
227+
/// Extracts the Java package name from a JNI type name.
228+
/// e.g., "com/example/MainActivity" → "com.example"
229+
/// Returns null for types without a package.
230+
/// </summary>
231+
internal static string? GetJavaPackageName (string jniName)
232+
{
233+
int lastSlash = jniName.LastIndexOf ('/');
234+
if (lastSlash < 0) {
235+
return null;
236+
}
237+
return jniName.Substring (0, lastSlash).Replace ('/', '.');
238+
}
239+
240+
/// <summary>
241+
/// Extracts the simple Java class name from a JNI type name.
242+
/// e.g., "com/example/MainActivity" → "MainActivity"
243+
/// e.g., "com/example/Outer$Inner" → "Outer$Inner" (preserves nesting separator)
244+
/// </summary>
245+
internal static string GetJavaSimpleName (string jniName)
246+
{
247+
int lastSlash = jniName.LastIndexOf ('/');
248+
return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName;
249+
}
250+
251+
/// <summary>
252+
/// Converts a JNI type descriptor to a Java source type.
253+
/// e.g., "V" → "void", "I" → "int", "Landroid/os/Bundle;" → "android.os.Bundle"
254+
/// </summary>
255+
internal static string JniTypeToJava (string jniType)
256+
{
257+
if (jniType.Length == 1) {
258+
return jniType [0] switch {
259+
'V' => "void",
260+
'Z' => "boolean",
261+
'B' => "byte",
262+
'C' => "char",
263+
'S' => "short",
264+
'I' => "int",
265+
'J' => "long",
266+
'F' => "float",
267+
'D' => "double",
268+
_ => throw new ArgumentException ($"Unknown JNI primitive type: {jniType}"),
269+
};
270+
}
271+
272+
// Array types: "[I" → "int[]", "[Ljava/lang/String;" → "java.lang.String[]"
273+
if (jniType [0] == '[') {
274+
return JniTypeToJava (jniType.Substring (1)) + "[]";
275+
}
276+
277+
// Object types: "Landroid/os/Bundle;" → "android.os.Bundle"
278+
if (jniType [0] == 'L' && jniType [jniType.Length - 1] == ';') {
279+
return JniNameToJavaName (jniType.Substring (1, jniType.Length - 2));
280+
}
281+
282+
throw new ArgumentException ($"Unknown JNI type descriptor: {jniType}");
283+
}
284+
}

0 commit comments

Comments
 (0)