Skip to content

Commit bb98ffc

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 b6371e0 commit bb98ffc

File tree

1 file changed

+298
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)