Skip to content

[TrimmableTypeMap] Add JCW Java source generator - #10830

Closed
simonrozsival wants to merge 8 commits into
dev/simonrozsival/trimmable-typemap-03b-uco-register-nativesfrom
dev/simonrozsival/trimmable-typemap-04-jcw-generator
Closed

[TrimmableTypeMap] Add JCW Java source generator#10830
simonrozsival wants to merge 8 commits into
dev/simonrozsival/trimmable-typemap-03b-uco-register-nativesfrom
dev/simonrozsival/trimmable-typemap-04-jcw-generator

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Feb 18, 2026

Copy link
Copy Markdown
Member

Part of #10799

Summary

Adds JCW (Java Callable Wrapper) Java source code generation for TypeMap proxy types.

Changes

  • JcwJavaSourceGenerator: generates .java source files for each ACW proxy type
    • Java class with extends/implements declarations
    • Constructor generation with super calls
    • Native method stubs for UCO wrappers
    • Uses $$"""...""" raw string literals for clean Java template output

Tests

  • Comprehensive tests for all JCW generation scenarios (basic types, abstract types, constructors, inheritance)

Stacked PRs:

  1. PR [TrimmableTypeMap] Add TypeMap proxy groundwork with CreateInstance #10808 — Proxy groundwork with CreateInstance
  2. PR [TrimmableTypeMap] Add UCO wrappers, RegisterNatives, and IAndroidCallableWrapper #10831 — UCO wrappers + RegisterNatives
  3. PR [TrimmableTypeMap] Add JCW Java source generator #10830this PR — JCW Java Source Generation

CopilotAI review requested due to automatic review settings February 18, 2026 17:08
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03-generators branch from 21a68db to c016cbeCompareFebruary 18, 2026 17:10
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from b278ff5 to c7e19fbCompareFebruary 18, 2026 17:10
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03-generators branch from c016cbe to 7800276CompareFebruary 18, 2026 17:12
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from c7e19fb to 4d1c7dcCompareFebruary 18, 2026 17:12

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new JCW (Java Callable Wrapper) Java source generator to the Microsoft.Android.Sdk.TrimmableTypeMap pipeline, intended to emit .java stubs from scanned JavaPeerInfo records, along with a new xUnit test suite covering generation scenarios.

Changes:

  • Introduces JcwJavaSourceGenerator to emit package/class declarations, constructors, marshal method wrappers, and native declarations.
  • Adds JcwJavaSourceGeneratorTests validating output formatting and key generation behaviors (constructors, methods, throws clauses, filtering).
  • Implements JNI-to-Java name/type conversion helpers used by the generator and tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.csNew generator responsible for producing JCW .java source files from JavaPeerInfo.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.csNew unit tests validating the generated Java output and conversion helpers.

Comment on lines +83 to +90
string className = GetJavaSimpleName (type.JavaName);

writer.Write ($"public {abstractModifier}class {className}\n");

// extends clause
if (type.BaseJavaName != null) {
writer.WriteLine ($"\textends {JniNameToJavaName (type.BaseJavaName)}");
}

CopilotAIFeb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generator mixes explicit \n (e.g., writer.Write(..."\n")) with TextWriter.WriteLine(), which uses writer.NewLine (platform-dependent). This can yield inconsistent line endings and makes the output non-deterministic across OSes. Consider using WriteLine() consistently after setting writer.NewLine = "\n", or consistently writing \n everywhere.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +127
var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
var java = GenerateToString (mainActivity);
Assert.StartsWith ("package my.app;\n", java);
}

CopilotAIFeb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion hard-codes \n line endings, but StringWriter/TextWriter.WriteLine() will use Environment.NewLine by default (e.g., \r\n on Windows). To keep the test cross-platform, normalize line endings (e.g., replace \r\n with \n) or assert using Assert.StartsWith($"package my.app;{Environment.NewLine}", java).

Copilot uses AI. Check for mistakes.
{
static string TestFixtureAssemblyPath {
get {
var testAssemblyDir = Path.GetDirectoryName (typeof (JcwJavaSourceGeneratorTests).Assembly.Location)!;

CopilotAIFeb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using the null-forgiving operator (!) for nullable handling. Path.GetDirectoryName(...) can be asserted non-null (or checked) instead, which keeps the test consistent with the repo's nullable practices.

Suggested change
vartestAssemblyDir=Path.GetDirectoryName(typeof(JcwJavaSourceGeneratorTests).Assembly.Location)!;
vartestAssemblyDir=Path.GetDirectoryName(typeof(JcwJavaSourceGeneratorTests).Assembly.Location);
Assert.NotNull(testAssemblyDir);

Copilot uses AI. Check for mistakes.
/// Generates JCW (Java Callable Wrapper) .java source files from scanned <see cref="JavaPeerInfo"/> records.
/// Only processes ACW types (where <see cref="JavaPeerInfo.DoNotGenerateAcw"/> is false).
/// </summary>
sealed class JcwJavaSourceGenerator

CopilotAIFeb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JcwJavaSourceGenerator is declared without an access modifier (internal), but the test project instantiates it directly. There is no InternalsVisibleTo for Microsoft.Android.Sdk.TrimmableTypeMap.Tests, so this will make the tests (and any external caller) unable to access the type. Consider making the generator public, or add an InternalsVisibleTo attribute for the test assembly if you want to keep the API internal.

Suggested change
sealedclass JcwJavaSourceGenerator
publicsealedclass JcwJavaSourceGenerator

Copilot uses AI. Check for mistakes.
var generatedFiles = new List<string> ();

foreach (var type in types) {
if (type.DoNotGenerateAcw) {

CopilotAIFeb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generate(IReadOnlyList<JavaPeerInfo> ...) only filters on DoNotGenerateAcw, but the scanner can return interface peers (JavaPeerInfo.IsInterface=true). Generating a class stub for an interface will produce invalid Java. Align the filtering with ModelBuilder (skip interfaces, and possibly only generate for actual ACW types).

Suggested change
if(type.DoNotGenerateAcw){
if(type.DoNotGenerateAcw||type.IsInterface){

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +172
// throws clause for [Export] methods
string throwsClause = "";
if (method.ThrownNames != null && method.ThrownNames.Count > 0) {
throwsClause = $"\n\t\tthrows {string.Join (", ", method.ThrownNames)}";
}

writer.Write ($$"""
@Override
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
{
{{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
}
public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});

CopilotAIFeb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated Java methods always include @Override. This will cause Java compilation failures for methods that do not actually override a supertype method (notably [Export] methods, which have Connector == null and are new methods on the stub). Consider omitting @Override entirely, or only emitting it when you can prove the method overrides something.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +66
// JNI name uses '/' as separator and '$' for nested types
// e.g., "com/example/MainActivity" → "com/example/MainActivity.java"
// Nested types: "com/example/Outer$Inner" → "com/example/Outer$Inner.java" (same file convention)
string relativePath = type.JavaName + ".java";
return Path.Combine (outputDirectory, relativePath);

CopilotAIFeb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetOutputFilePath uses type.JavaName directly to build a relative path. Since JavaName ultimately comes from user-controlled [Register] strings, a malicious value containing .. or rooted paths could write files outside outputDirectory (path traversal). Validate/sanitize JavaName (e.g., reject .., drive roots, and invalid path chars) and ensure the final Path.GetFullPath stays under outputDirectory.

Suggested change
// JNI name uses '/' as separator and '$' for nested types
// e.g., "com/example/MainActivity" → "com/example/MainActivity.java"
// Nested types: "com/example/Outer$Inner" → "com/example/Outer$Inner.java" (same file convention)
stringrelativePath=type.JavaName+".java";
returnPath.Combine(outputDirectory,relativePath);
if(type.JavaNameisnull){
thrownewInvalidOperationException("JavaName cannot be null when generating JCW source.");
}
stringjavaName=type.JavaName;
// Basic validation to prevent path traversal and rooted paths.
// JavaName is expected to be a JNI-style class name (e.g., "com/example/MainActivity").
if(javaName.Length==0){
thrownewInvalidOperationException("JavaName cannot be empty when generating JCW source.");
}
// Disallow parent-directory traversal.
if(javaName.Contains("..",StringComparison.Ordinal)){
thrownewInvalidOperationException($"JavaName '{javaName}' is invalid: must not contain \"..\".");
}
// Disallow rooted or drive-qualified paths.
if(Path.IsPathRooted(javaName)){
thrownewInvalidOperationException($"JavaName '{javaName}' is invalid: must not be a rooted path.");
}
// Disallow backslashes and drive separators (e.g., 'C:foo').
if(javaName.IndexOfAny(new[]{'\\',':'})>=0){
thrownewInvalidOperationException($"JavaName '{javaName}' is invalid: contains illegal path characters.");
}
// Disallow any other invalid path characters (excluding '/' which is valid in JNI names).
char[]invalidPathChars=Path.GetInvalidPathChars();
for(inti=0;i<javaName.Length;i++){
charc=javaName[i];
if(c=='/'){
continue;
}
if(Array.IndexOf(invalidPathChars,c)>=0){
thrownewInvalidOperationException($"JavaName '{javaName}' is invalid: contains illegal path characters.");
}
}
stringfullOutputDir=Path.GetFullPath(outputDirectory);
// JNI name uses '/' as separator and '$' for nested types
// e.g., "com/example/MainActivity" → "com/example/MainActivity.java"
// Nested types: "com/example/Outer$Inner" → "com/example/Outer$Inner.java" (same file convention)
stringrelativePath=javaName+".java";
stringcombinedPath=Path.Combine(fullOutputDir,relativePath);
stringfullPath=Path.GetFullPath(combinedPath);
// Ensure the resulting path stays within the output directory.
if(!fullPath.StartsWith(fullOutputDir,StringComparison.OrdinalIgnoreCase)){
thrownewInvalidOperationException($"JavaName '{javaName}' is invalid: resulting path is outside the output directory.");
}
if(fullPath.Length>fullOutputDir.Length){
charseparator=fullPath[fullOutputDir.Length];
if(separator!=Path.DirectorySeparatorChar&&separator!=Path.AltDirectorySeparatorChar){
thrownewInvalidOperationException($"JavaName '{javaName}' is invalid: resulting path is outside the output directory.");
}
}
returnfullPath;

Copilot uses AI. Check for mistakes.
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 4d1c7dc to ce40756CompareFebruary 18, 2026 17:27
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03-generators branch from 7800276 to 7b5ecb7CompareFebruary 18, 2026 17:27
@simonrozsival
simonrozsival changed the base branch from dev/simonrozsival/trimmable-typemap-03-generators to dev/simonrozsival/trimmable-typemap-03b-uco-register-nativesFebruary 18, 2026 17:28
@simonrozsival
simonrozsival marked this pull request as draft February 18, 2026 17:38
@simonrozsivalsimonrozsival added trimmable-type-map Area: NativeAOT Issues that only occur when using NativeAOT. Area: CoreCLR Issues that only occur when using CoreCLR. copilot `copilot-cli` or other AIs were used to author this labels Feb 18, 2026
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from 132b798 to 9914c6eCompareFebruary 18, 2026 18:42
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from ce40756 to b981e2bCompareFebruary 18, 2026 18:42
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch 3 times, most recently from d18059a to a235422CompareFebruary 18, 2026 23:13
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch 2 times, most recently from 630b46c to 8e01ea4CompareFebruary 18, 2026 23:15
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from e2a6a83 to cd59ff8CompareFebruary 18, 2026 23:46
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 8e01ea4 to ff2f0b7CompareFebruary 18, 2026 23:46
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from cd59ff8 to 5cf8820CompareFebruary 18, 2026 23:52
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from ff2f0b7 to 640d283CompareFebruary 18, 2026 23:52
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 2c1267a to 689d566CompareFebruary 19, 2026 10:03
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from 10e549d to f2a2cfaCompareFebruary 19, 2026 10:09
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 689d566 to 796c30dCompareFebruary 19, 2026 10:09
simonrozsival added a commit that referenced this pull request Feb 19, 2026
This entire test file references JcwJavaSourceGenerator which doesn't
exist in this PR or anywhere in the repo yet. It belongs with PR #10830
(JCW Java Source Generation) which introduces that class.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Feb 19, 2026
Move UCO-specific JniSignatureHelper methods (JniParamKind, ParseParameterTypes,
ParseReturnType, EncodeClrType, ParseSingleType) and their tests to PR #10831.
Move JCW-specific scanner properties (BaseJavaName, ImplementedInterfaceJavaNames,
JavaConstructors, JavaConstructorInfo) and resolver methods (ResolveBaseJavaName,
ResolveImplementedInterfaceJavaNames, ResolveInterfaceJniName, BuildJavaConstructors)
to PR #10830.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from f2a2cfa to b709256CompareFebruary 19, 2026 10:59
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 5c45a22 to 4b812eeCompareFebruary 19, 2026 11:01
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from b709256 to ca74680CompareFebruary 19, 2026 12:00
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 4b812ee to 1769280CompareFebruary 19, 2026 12:03
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from ca74680 to 2574cc1CompareFebruary 19, 2026 12:07
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch 2 times, most recently from b67e2d7 to 001be98CompareFebruary 19, 2026 12:35
simonrozsivaland others added 2 commits February 19, 2026 14:26
…lableWrapper
Add ACW (Android Callable Wrapper) support to TypeMap proxy types:
- UcoMethodData/UcoConstructorData/NativeRegistrationData model types
- ModelBuilder: generates UCO method wrappers, constructor wrappers,
and native method registrations for ACW types
- TypeMapAssemblyEmitter: emits [UnmanagedCallersOnly] UCO methods,
UCO constructor wrappers, RegisterNatives, IAndroidCallableWrapper
- IsAcw detection for types that need JNI native method registration
- Cross-assembly IgnoresAccessChecksTo for UCO callback types
- Tests for all UCO/RegisterNatives generation scenarios
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add JniParamKind enum, ParseParameterTypes, ParseReturnType,
EncodeClrType, ParseSingleType, and their test classes
(JniSignatureHelperTests, NegativeEdgeCase) — moved from PR #10808.
Also add back JavaConstructors property, JavaConstructorInfo record,
and BuildJavaConstructors scanner method needed by ModelBuilder.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from 8bc6d91 to 1b0a135CompareFebruary 19, 2026 13:28
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 001be98 to 97d89bfCompareFebruary 19, 2026 13:29
- Fix UCO attribute test to verify actual UnmanagedCallersOnlyAttribute
type name instead of just Assert.NotEmpty(attrs)
- Replace non-asserting NegativeEdgeCase Theory with three Facts that
each assert a specific expected behavior
- Remove duplicate SkipSingleType method; ParseParameterTypeStrings
now reuses ParseSingleType to advance the index
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from 1b0a135 to a59275cCompareFebruary 19, 2026 13:29
simonrozsivaland others added 5 commits February 19, 2026 14:29
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>
Generate_CustomView_HasExpectedConstructorElement and
Generate_TouchHandler_HasExpectedMethodSignature were Theories that
called GenerateFixture with the same input for each InlineData case,
needlessly repeating the expensive fixture scan+generate. Convert to
single Facts with multiple Assert.Contains calls.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add BaseJavaName, ImplementedInterfaceJavaNames properties to JavaPeerInfo,
and ResolveBaseJavaName, ResolveImplementedInterfaceJavaNames,
ResolveInterfaceJniName methods to JavaPeerScanner — moved from PR #10808.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace try-finally cleanup with IDisposable temp directories
- Add IGCUserPeer infrastructure methods (monodroidAddReference,
monodroidClearReferences, refList) to JCW Java source generator
- Port improved UCO attribute and NegativeEdgeCase tests from #10831
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-04-jcw-generator branch from 97d89bf to 0e5c561CompareFebruary 19, 2026 13:30
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/trimmable-typemap-03b-uco-register-natives branch from a59275c to 0a7c7b6CompareMarch 10, 2026 10:43
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Closing this stale draft — it will be revisited once the base PR #10808 is merged.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Area: CoreCLRIssues that only occur when using CoreCLR.Area: NativeAOTIssues that only occur when using NativeAOT.copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@simonrozsival