From 35ccc4ff269b28d1a1646544785e26f6442b9bc7 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:00:34 -0500 Subject: [PATCH 1/6] [class-parse] Detect Kotlin @JvmInline value classes Phase 2 of dotnet/java-interop#1431. Surface inline-class info on the api.xml so the generator can later project parameters/returns to strongly-typed wrapper structs while keeping JNI marshaling on the underlying primitive. * Stamp ClassFile.KotlinInlineClassUnderlyingJniType with the JNI descriptor of the single non-synthetic instance field on every Kotlin '@JvmInline value class'. * Stamp MethodInfo.KotlinInlineClassReturnJniType / ParameterInfo. KotlinInlineClassJniType when a method's Kotlin source-level return or parameter type was an inline class (the JVM-erased type is the inline class's backing primitive). * Emit kotlin-inline-class / kotlin-inline-class-underlying-jni-type on , kotlin-inline-class-jni-type on , and kotlin-inline-class-return-jni-type on . * Bytecode tests cover the existing kotlin-gradle/ MyColor / MyAlpha / MyDp / Widgets fixture: ULong-backed and Float-backed detection, per-parameter stamping, return-type stamping, and round-trip through XmlClassDeclarationBuilder. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ClassFile.cs | 6 + .../Kotlin/KotlinFixups.cs | 99 ++++++++++++++- src/Xamarin.Android.Tools.Bytecode/Methods.cs | 16 +++ .../XmlClassDeclarationBuilder.cs | 35 ++++++ .../KotlinInlineClassCollisionTests.cs | 113 ++++++++++++++++++ ...marin.Android.Tools.Bytecode-Tests.targets | 1 - 6 files changed, 264 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Tools.Bytecode/ClassFile.cs b/src/Xamarin.Android.Tools.Bytecode/ClassFile.cs index ebd012291..c63f7e9c0 100644 --- a/src/Xamarin.Android.Tools.Bytecode/ClassFile.cs +++ b/src/Xamarin.Android.Tools.Bytecode/ClassFile.cs @@ -24,6 +24,12 @@ public sealed class ClassFile { public Methods Methods; public AttributeCollection Attributes; + // Set by KotlinFixups when this class is a Kotlin `@JvmInline value class`. + // The value is the JNI type descriptor of the single backing field + // (e.g. "J", "F", "I", "Ljava/lang/String;"). null otherwise. + // See dotnet/java-interop#1431 (Phase 2). + public string? KotlinInlineClassUnderlyingJniType { get; set; } + ClassSignature? signature; diff --git a/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs b/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs index 56ded5131..8cce2c98b 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs +++ b/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs @@ -11,6 +11,13 @@ public static class KotlinFixups { public static void Fixup (IList classes) { + // Pre-pass: identify Kotlin `@JvmInline value class` types and record + // each one's JNI name -> backing primitive descriptor. We need this + // map before processing methods so that a method on class A that takes + // an inline class B as a parameter (via Kotlin metadata) can be stamped + // even if B is later in `classes`. See dotnet/java-interop#1431. + var inlineClasses = DetectInlineClasses (classes); + foreach (var c in classes) { // See if this is a Kotlin class var attr = c.Attributes.OfType ().FirstOrDefault (); @@ -51,7 +58,7 @@ public static void Fixup (IList classes) // and we need to find the "best" match for each Java method. foreach (var java_method in c.Methods) if (FindKotlinFunctionMetadata (metadata, java_method) is KotlinFunction function_metadata) - FixupFunction (java_method, function_metadata, class_metadata); + FixupFunction (java_method, function_metadata, class_metadata, inlineClasses); } if (metadata.Properties != null) { @@ -59,7 +66,7 @@ public static void Fixup (IList classes) var getter = FindJavaPropertyGetter (metadata, prop, c); var setter = FindJavaPropertySetter (metadata, prop, c); - FixupProperty (getter, setter, prop); + FixupProperty (getter, setter, prop, inlineClasses); FixupField (FindJavaFieldProperty (metadata, prop, c), prop); } @@ -71,6 +78,75 @@ public static void Fixup (IList classes) } } + // Identifies Kotlin `@JvmInline value class` types in `classes` and stamps + // each `ClassFile.KotlinInlineClassUnderlyingJniType` with the JNI descriptor + // of its single backing field. Returns a map from the class's *Kotlin metadata* + // class-name representation (e.g. `com/example/MyColor;`) to that descriptor, + // for use when projecting `KotlinType.ClassName` references on parameters and + // return types of OTHER methods. See dotnet/java-interop#1431 (Phase 2). + static Dictionary DetectInlineClasses (IList classes) + { + var map = new Dictionary (StringComparer.Ordinal); + foreach (var c in classes) { + var ann = c.Attributes.OfType ().FirstOrDefault (); + if (ann is null) + continue; + + // `@JvmInline` is the JVM-level marker for Kotlin inline/value classes. + if (!ann.Annotations.Any (a => a.Type == "Lkotlin/jvm/JvmInline;")) + continue; + + // Sanity-check via Kotlin metadata: must be `kind == 1` (Class) and + // have IsInlineClass set. This filters out `@JvmInline` on things + // kotlinc may have emitted in the future for non-class kinds. + var meta = ann.Annotations.SingleOrDefault (a => a.Type == "Lkotlin/Metadata;"); + if (meta is null) + continue; + + try { + var km = KotlinMetadata.FromAnnotation (meta); + if (km.AsClassMetadata () is not KotlinClass kc) + continue; + if ((kc.Flags & KotlinClassFlags.IsInlineClass) == 0) + continue; + + // The single non-synthetic, non-static instance field is the + // inline-class backing value. (Synthetic fields like `Companion` + // are filtered out.) + var backing = c.Fields.FirstOrDefault (f => + !f.AccessFlags.HasFlag (FieldAccessFlags.Synthetic) && + !f.AccessFlags.HasFlag (FieldAccessFlags.Static)); + if (backing is null) + continue; + + c.KotlinInlineClassUnderlyingJniType = backing.Descriptor; + + // Kotlin's `KotlinType.ClassName` strings are stored without the + // leading `L` but with a trailing `;` (e.g. `com/example/MyColor;`). + // We index by that form so callers can look up directly from + // `kotlin_p.Type.ClassName` without string surgery. + var jvmName = c.ThisClass.Name.Value + ";"; + map [jvmName] = backing.Descriptor; + } catch (Exception ex) { + Log.Warning (0, $"class-parse: warning: Unable to detect inline class on '{c.ThisClass.Name}': {ex}"); + } + } + return map; + } + + // JNI signature for the Kotlin inline class referenced by `kotlinTypeClassName`, + // or null when the name is unknown / not an inline class. The returned form + // has a leading `L` and trailing `;` so it matches `ClassFile.FullJniName` + // and other JNI-signature strings used throughout the pipeline. + static string? GetInlineClassJniType (string? kotlinTypeClassName, IDictionary inlineClasses) + { + if (kotlinTypeClassName is null) + return null; + if (!inlineClasses.ContainsKey (kotlinTypeClassName)) + return null; + return "L" + kotlinTypeClassName; + } + static void FixupClassVisibility (ClassFile klass, KotlinClass metadata) { // Hide class if it isn't Public/Protected @@ -179,7 +255,7 @@ static void FixupConstructor (MethodInfo? method, KotlinConstructor metadata) } } - static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinClass? kotlinClass) + static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinClass? kotlinClass, IDictionary inlineClasses) { if (method is null || !method.IsPubliclyVisible) return; @@ -209,10 +285,20 @@ static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinCl // Handle erasure of Kotlin unsigned types java_p.KotlinType = GetKotlinType (java_p.Type.TypeSignature, kotlin_p.Type.ClassName); + + // Inline-class projection: if the Kotlin source-level type for this + // parameter is a `@JvmInline value class` we know about, record its + // JNI signature so the generator can later swap the parameter type + // for a strongly-typed wrapper struct while keeping JNI marshaling + // on the underlying primitive. See dotnet/java-interop#1431 (Phase 2). + java_p.KotlinInlineClassJniType = GetInlineClassJniType (kotlin_p.Type.ClassName, inlineClasses); } // Handle erasure of Kotlin unsigned types method.KotlinReturnType = GetKotlinType (method.ReturnType.TypeSignature, metadata.ReturnType?.ClassName); + + // Same projection as above, but for the return type. + method.KotlinInlineClassReturnJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, inlineClasses); } public static (int start, int end) CreateParameterMap (MethodInfo method, KotlinFunction function, KotlinClass? kotlinClass) @@ -267,7 +353,7 @@ static void FixupExtensionMethod (MethodInfo method) } } - static void FixupProperty (MethodInfo? getter, MethodInfo? setter, KotlinProperty metadata) + static void FixupProperty (MethodInfo? getter, MethodInfo? setter, KotlinProperty metadata, IDictionary inlineClasses) { if (getter is null && setter is null) return; @@ -289,8 +375,10 @@ static void FixupProperty (MethodInfo? getter, MethodInfo? setter, KotlinPropert } // Handle erasure of Kotlin unsigned types - if (getter != null) + if (getter != null) { getter.KotlinReturnType = GetKotlinType (getter.ReturnType.TypeSignature, metadata.ReturnType?.ClassName); + getter.KotlinInlineClassReturnJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, inlineClasses); + } if (setter != null) { var setter_parameter = setter.GetParameters ().First (); @@ -302,6 +390,7 @@ static void FixupProperty (MethodInfo? getter, MethodInfo? setter, KotlinPropert // Handle erasure of Kotlin unsigned types setter_parameter.KotlinType = GetKotlinType (setter_parameter.Type.TypeSignature, metadata.ReturnType?.ClassName); + setter_parameter.KotlinInlineClassJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, inlineClasses); } } diff --git a/src/Xamarin.Android.Tools.Bytecode/Methods.cs b/src/Xamarin.Android.Tools.Bytecode/Methods.cs index a0fdfba78..e157fc72a 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Methods.cs +++ b/src/Xamarin.Android.Tools.Bytecode/Methods.cs @@ -35,6 +35,14 @@ public sealed class MethodInfo { public AttributeCollection Attributes {get; private set;} public string? KotlinReturnType {get; set;} + // JNI signature of the Kotlin `@JvmInline` class that this method's return + // type was originally declared as in Kotlin source, when the JVM-erased + // return type is the inline class's underlying primitive. e.g. + // `Lcom/example/MyColor;` for a method declared `fun f(): MyColor` whose + // JVM signature is `()J`. null when no projection applies. + // See dotnet/java-interop#1431 (Phase 2). + public string? KotlinInlineClassReturnJniType { get; set; } + public MethodInfo (ConstantPool constantPool, ClassFile declaringType, Stream stream) { ConstantPool = constantPool; @@ -378,6 +386,14 @@ public sealed class ParameterInfo : IEquatable { public TypeInfo Type; public string? KotlinType; + // JNI signature of the Kotlin `@JvmInline` class that this parameter was + // originally declared as in Kotlin source, when the JVM-erased parameter + // type is the inline class's underlying primitive. e.g. + // `Lcom/example/MyColor;` for a Kotlin parameter declared `color: MyColor` + // whose JVM type is `J`. null when no projection applies. + // See dotnet/java-interop#1431 (Phase 2). + public string? KotlinInlineClassJniType; + public MethodParameterAccessFlags AccessFlags; public ParameterInfo (string name, string binaryName, string? typeSignature = null, int position = 0) diff --git a/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs b/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs index b6df82728..ffe0bed21 100644 --- a/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs +++ b/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs @@ -41,6 +41,7 @@ public XElement ToXElement () new XAttribute ("static", classFile.IsStatic), new XAttribute ("visibility", GetVisibility (classFile.Visibility)), GetAnnotatedVisibility (classFile.Visibility, classFile.Attributes), + GetKotlinInlineClassAttributes (), GetTypeParmeters (signature == null ? null : signature.TypeParameters), GetImplementedInterfaces (), GetConstructors (), @@ -49,6 +50,18 @@ public XElement ToXElement () ); } + // dotnet/java-interop#1431 (Phase 2): when this class is a Kotlin + // `@JvmInline value class`, emit two extra attributes that drive + // generator-side wrapper-struct emission and parameter/return projection. + IEnumerable GetKotlinInlineClassAttributes () + { + var underlying = classFile.KotlinInlineClassUnderlyingJniType; + if (string.IsNullOrEmpty (underlying)) + yield break; + yield return new XAttribute ("kotlin-inline-class", "true"); + yield return new XAttribute ("kotlin-inline-class-underlying-jni-type", underlying!); + } + string GetElementName () { if (IsInterface) @@ -346,6 +359,7 @@ XElement GetMethod (string element, string name, MethodInfo method, string? retu GetNative (method), ret, jniRet, + GetKotlinInlineClassReturnJniType (method), new XAttribute ("static", (method.AccessFlags & MethodAccessFlags.Static) != 0), GetSynchronized (method), new XAttribute ("visibility", GetVisibility (method.AccessFlags)), @@ -359,6 +373,16 @@ XElement GetMethod (string element, string name, MethodInfo method, string? retu GetExceptions (method)); } + // dotnet/java-interop#1431 (Phase 2): when a method's Kotlin source-level + // return type was a `@JvmInline value class`, surface that type's JNI + // signature so the generator can project the return type to a wrapper struct. + static XAttribute? GetKotlinInlineClassReturnJniType (MethodInfo method) + { + if (string.IsNullOrEmpty (method.KotlinInlineClassReturnJniType)) + return null; + return new XAttribute ("kotlin-inline-class-return-jni-type", method.KotlinInlineClassReturnJniType!); + } + static XAttribute? GetNative (MethodInfo method) { if (method.IsConstructor) @@ -415,10 +439,21 @@ IEnumerable GetMethodParameters (MethodInfo method) new XAttribute ("name", p.Name), new XAttribute ("type", genericType), new XAttribute ("jni-type", p.Type.TypeSignature ?? p.Type.BinaryName), + GetKotlinInlineClassJniType (p), GetNotNull (annotations, i)); } } + // dotnet/java-interop#1431 (Phase 2): when the parameter's Kotlin source-level + // type was a `@JvmInline value class`, surface that type's JNI signature so + // the generator can project the parameter to a strongly-typed wrapper struct. + static XAttribute? GetKotlinInlineClassJniType (ParameterInfo p) + { + if (string.IsNullOrEmpty (p.KotlinInlineClassJniType)) + return null; + return new XAttribute ("kotlin-inline-class-jni-type", p.KotlinInlineClassJniType!); + } + IEnumerable GetExceptions (MethodInfo method) { foreach (var t in method.GetThrows ()) { diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinInlineClassCollisionTests.cs b/tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinInlineClassCollisionTests.cs index 3a26fcd1f..106b875e7 100644 --- a/tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinInlineClassCollisionTests.cs +++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinInlineClassCollisionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Xamarin.Android.Tools.Bytecode; @@ -66,5 +67,117 @@ public void InlineClasses_AreEmittedAsValueClasses () Assert.Contains ("Lkotlin/jvm/JvmInline;", annotations); } + + // dotnet/java-interop#1431 (Phase 2): KotlinFixups must surface the inline + // class's underlying primitive on each `@JvmInline value class` ClassFile so + // the generator can later emit a strongly-typed wrapper struct. + [Test] + public void Fixup_StampsKotlinInlineClassUnderlyingJniType () + { + var classes = LoadInlineClassFixture (); + + KotlinFixups.Fixup (classes); + + var byName = classes.ToDictionary (c => c.ThisClass.Name.Value); + + // MyColor and MyAlpha are both ULong-backed -> JNI primitive `J`. + Assert.AreEqual ("J", byName ["xat/bytecode/tests/MyColor"].KotlinInlineClassUnderlyingJniType); + Assert.AreEqual ("J", byName ["xat/bytecode/tests/MyAlpha"].KotlinInlineClassUnderlyingJniType); + + // MyDp is Float-backed -> JNI primitive `F`. + Assert.AreEqual ("F", byName ["xat/bytecode/tests/MyDp"].KotlinInlineClassUnderlyingJniType); + + // Non-inline classes must NOT be stamped. + Assert.IsNull (byName ["xat/bytecode/tests/Widgets"].KotlinInlineClassUnderlyingJniType); + } + + // dotnet/java-interop#1431 (Phase 2): for every Kotlin function whose + // source-level parameter type is a known inline class, KotlinFixups must + // stamp the `KotlinInlineClassJniType` on that parameter so the generator + // can swap the parameter's symbol for a wrapper-struct projection while + // keeping JNI marshaling on the underlying primitive. + [Test] + public void Fixup_StampsParameterInlineClassJniType () + { + var classes = LoadInlineClassFixture (); + KotlinFixups.Fixup (classes); + + var widgets = classes.Single (c => c.ThisClass.Name.Value == "xat/bytecode/tests/Widgets"); + + var tints = widgets.Methods + .Where (m => m.Name.StartsWith ("tint-", StringComparison.Ordinal)) + .ToList (); + + // Each tint() should have exactly one parameter, and that parameter + // should be stamped with the JNI signature of the inline class it + // originally came from in Kotlin source. + var stampedJniTypes = tints + .Select (m => m.GetParameters ().Single ().KotlinInlineClassJniType) + .Where (j => !string.IsNullOrEmpty (j)) + .OrderBy (j => j, StringComparer.Ordinal) + .ToList (); + + CollectionAssert.AreEquivalent ( + new [] { + "Lxat/bytecode/tests/MyAlpha;", + "Lxat/bytecode/tests/MyColor;", + "Lxat/bytecode/tests/MyDp;", + }, + stampedJniTypes, + "Each tint() parameter must be stamped with its inline-class JNI signature."); + } + + // dotnet/java-interop#1431 (Phase 2): when a method's Kotlin-source-level + // return type is a `@JvmInline value class`, KotlinFixups must stamp the + // method's `KotlinInlineClassReturnJniType` so the generator can project + // the return type to the wrapper struct. + [Test] + public void Fixup_StampsReturnInlineClassJniType () + { + var classes = LoadInlineClassFixture (); + KotlinFixups.Fixup (classes); + + var widgets = classes.Single (c => c.ThisClass.Name.Value == "xat/bytecode/tests/Widgets"); + + var pads = widgets.Methods + .Where (m => m.Name.StartsWith ("pad-", StringComparison.Ordinal)) + .ToList (); + + Assert.IsTrue (pads.Count > 0, "Expected `pad` overloads in fixture."); + Assert.IsTrue ( + pads.All (m => m.KotlinInlineClassReturnJniType == "Lxat/bytecode/tests/MyDp;"), + $"All `pad` overloads return MyDp; got: [{string.Join (", ", pads.Select (m => m.KotlinInlineClassReturnJniType ?? ""))}]"); + } + + // dotnet/java-interop#1431 (Phase 2): the new fields must round-trip + // through XmlClassDeclarationBuilder onto the api.xml that the generator + // consumes. + [Test] + public void XmlOutput_ContainsKotlinInlineClassAttributes () + { + var classes = LoadInlineClassFixture (); + KotlinFixups.Fixup (classes); + + var classPath = new ClassPath { ApiSource = "class-parse" }; + foreach (var c in classes) + classPath.Add (c); + + var sw = new System.IO.StringWriter (); + classPath.SaveXmlDescription (sw); + var xml = sw.ToString (); + + StringAssert.Contains ("kotlin-inline-class=\"true\"", xml); + StringAssert.Contains ("kotlin-inline-class-underlying-jni-type=\"J\"", xml); + StringAssert.Contains ("kotlin-inline-class-underlying-jni-type=\"F\"", xml); + StringAssert.Contains ("kotlin-inline-class-jni-type=\"Lxat/bytecode/tests/MyColor;\"", xml); + StringAssert.Contains ("kotlin-inline-class-return-jni-type=\"Lxat/bytecode/tests/MyDp;\"", xml); + } + + static List LoadInlineClassFixture () => new List { + LoadClassFile ("MyColor.class"), + LoadClassFile ("MyAlpha.class"), + LoadClassFile ("MyDp.class"), + LoadClassFile ("Widgets.class"), + }; } } diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets b/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets index c973c22db..d9965fe89 100644 --- a/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets +++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets @@ -10,7 +10,6 @@ - From 41c66f7d3e173856d226abfdd38f3b5295db9230 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 3 Jun 2026 11:13:47 -0500 Subject: [PATCH 2/6] [generator] Project Kotlin inline classes as readonly struct wrappers Phase 2 of #1431. Wires the inline-class XML attributes from the class-parse layer (commit 35ccc4ff) through the generator so: * `` is emitted as a `readonly partial struct` wrapper around the underlying primitive (e.g. `J` -> `long`, `F` -> `float`) instead of a peer-class binding. The struct exposes `Value`, implicit conversions, equality, and ToString. * `` makes the generator project the parameter's managed type to the wrapper struct while keeping JNI marshaling on the underlying primitive. Because the struct has implicit conversion operators, existing `JniArgumentValue` thunks compile unchanged. * `` does the same for return values via `ReturnValue.managed_type`. Plumbing: * `ClassGen.IsKotlinInlineClass`, `ClassGen.KotlinInlineClassUnderlyingJniType` * `Parameter.KotlinInlineClassJniType` * `Method.KotlinInlineClassReturnJniType` * `Parameter.Validate`/`ReturnValue.Validate` apply the projection by looking up the wrapper `ClassGen` via `SymbolTable`. * New `KotlinInlineClassStruct` TypeWriter emits the wrapper struct. * New `TypeNameUtilities.JniSignatureToJavaTypeName` helper. Out of scope (acknowledged limitations): boxed/nullable/generic inline-class positions still resolve to the (now-replaced) peer binding; reference-backed inline classes; generic inline classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Unit-Tests/KotlinInlineClassTests.cs | 101 ++++++++++++++++++ .../JavaInteropCodeGenerator.cs | 6 +- .../XmlApiImporter.cs | 8 +- .../ClassGen.cs | 8 ++ .../Method.cs | 2 + .../Parameter.cs | 29 ++++- .../ReturnValue.cs | 19 ++++ .../SourceWriters/KotlinInlineClassStruct.cs | 79 ++++++++++++++ .../generator/Utilities/TypeNameUtilities.cs | 13 +++ 9 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 tests/generator-Tests/Unit-Tests/KotlinInlineClassTests.cs create mode 100644 tools/generator/SourceWriters/KotlinInlineClassStruct.cs diff --git a/tests/generator-Tests/Unit-Tests/KotlinInlineClassTests.cs b/tests/generator-Tests/Unit-Tests/KotlinInlineClassTests.cs new file mode 100644 index 000000000..2b83ba518 --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/KotlinInlineClassTests.cs @@ -0,0 +1,101 @@ +using System.Linq; +using System.Xml.Linq; +using Java.Interop.Tools.Generator; +using MonoDroid.Generation; +using NUnit.Framework; + +namespace generatortests +{ + [TestFixture] + public class KotlinInlineClassTests + { + CodeGenerationOptions opt = new CodeGenerationOptions (); + + [Test] + public void CreateClass_ReadsKotlinInlineClassAttributes () + { + var xml = XDocument.Parse ( + "" + + "" + + ""); + + var klass = XmlApiImporter.CreateClass (xml.Root, xml.Root.Element ("class"), opt); + + Assert.IsTrue (klass.IsKotlinInlineClass); + Assert.AreEqual ("J", klass.KotlinInlineClassUnderlyingJniType); + } + + [Test] + public void CreateClass_DefaultsForNonInlineClass () + { + var xml = XDocument.Parse ( + "" + + "" + + ""); + + var klass = XmlApiImporter.CreateClass (xml.Root, xml.Root.Element ("class"), opt); + + Assert.IsFalse (klass.IsKotlinInlineClass); + } + + [Test] + public void CreateParameter_ReadsKotlinInlineClassJniType () + { + var xml = XDocument.Parse ( + ""); + + var p = XmlApiImporter.CreateParameter (xml.Root, opt); + + Assert.AreEqual ("Lcom/example/MyColor;", p.KotlinInlineClassJniType); + } + + [Test] + public void CreateMethod_ReadsKotlinInlineClassReturnJniType () + { + var xml = XDocument.Parse ( + "" + + "" + + "" + + ""); + + var pkg = xml.Root; + var classElem = pkg.Element ("class"); + var klass = XmlApiImporter.CreateClass (pkg, classElem, opt); + var method = klass.Methods.First (); + + Assert.AreEqual ("Lcom/example/MyDp;", method.KotlinInlineClassReturnJniType); + } + + [Test] + public void Parameter_Clone_PreservesKotlinInlineClassJniType () + { + var xml = XDocument.Parse ( + ""); + + var p = XmlApiImporter.CreateParameter (xml.Root, opt); + var clone = p.Clone (); + + Assert.AreEqual ("Lcom/example/MyColor;", clone.KotlinInlineClassJniType); + } + + [Test] + public void TypeNameUtilities_JniSignatureToJavaTypeName_HandlesReferenceTypes () + { + Assert.AreEqual ("com.example.MyColor", + TypeNameUtilities.JniSignatureToJavaTypeName ("Lcom/example/MyColor;")); + Assert.AreEqual ("java.lang.String", + TypeNameUtilities.JniSignatureToJavaTypeName ("Ljava/lang/String;")); + } + + [Test] + public void TypeNameUtilities_JniSignatureToJavaTypeName_RejectsPrimitivesAndArrays () + { + Assert.IsNull (TypeNameUtilities.JniSignatureToJavaTypeName ("J")); + Assert.IsNull (TypeNameUtilities.JniSignatureToJavaTypeName ("")); + Assert.IsNull (TypeNameUtilities.JniSignatureToJavaTypeName (null)); + // Array types intentionally not supported by inline-class projection. + Assert.IsNull (TypeNameUtilities.JniSignatureToJavaTypeName ("[Lcom/example/MyColor;")); + } + } +} diff --git a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs index d13c9fb31..8cb5770cc 100644 --- a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs +++ b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs @@ -42,8 +42,10 @@ public virtual void WriteType (GenBase gen, string indent, GenerationInfo gen_in if (gen is InterfaceGen iface) type_writer = new BoundInterface (iface, opt, Context, gen_info); - else if (gen is ClassGen klass) - type_writer = new BoundClass (klass, opt, Context, gen_info); + else if (gen is ClassGen klass && klass.IsKotlinInlineClass) + type_writer = new KotlinInlineClassStruct (klass, opt); + else if (gen is ClassGen klass2) + type_writer = new BoundClass (klass2, opt, Context, gen_info); else throw new InvalidOperationException ("Unknown GenBase type"); diff --git a/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs b/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs index 5016f66be..ffc3dd602 100644 --- a/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs +++ b/tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs @@ -110,6 +110,8 @@ public static ClassGen CreateClass (XElement pkg, XElement elem, CodeGenerationO FromXml = true, IsAbstract = elem.XGetAttribute ("abstract") == "true", IsFinal = elem.XGetAttribute ("final") == "true", + IsKotlinInlineClass = elem.XGetAttribute ("kotlin-inline-class") == "true", + KotlinInlineClassUnderlyingJniType = elem.XGetAttribute ("kotlin-inline-class-underlying-jni-type"), PeerConstructorPartialMethod = elem.XGetAttribute ("peerConstructorPartialMethod"), // Only use an explicitly set XML attribute Unnest = elem.XGetAttribute ("unnest") == "true" ? true : @@ -390,6 +392,7 @@ public static Method CreateMethod (GenBase declaringType, XElement elem, CodeGen JavaName = elem.XGetAttribute ("name"), ManagedOverride = elem.XGetAttribute ("managedOverride"), ManagedReturn = elem.XGetAttribute ("managedReturn"), + KotlinInlineClassReturnJniType = elem.Attribute ("kotlin-inline-class-return-jni-type") != null ? elem.XGetAttribute ("kotlin-inline-class-return-jni-type") : null, PropertyNameOverride = elem.XGetAttribute ("propertyName"), Return = elem.XGetAttribute ("return"), ReturnNotNull = elem.XGetAttribute ("return-not-null") == "true", @@ -445,8 +448,11 @@ public static Parameter CreateParameter (XElement elem, CodeGenerationOptions op string enum_type = elem.Attribute ("enumType") != null ? elem.XGetAttribute ("enumType") : null; string managed_type = elem.Attribute ("managedType") != null ? elem.XGetAttribute ("managedType") : null; var not_null = elem.XGetAttribute ("not-null") == "true"; + string kotlin_inline_jni = elem.Attribute ("kotlin-inline-class-jni-type") != null ? elem.XGetAttribute ("kotlin-inline-class-jni-type") : null; // FIXME: "enum_type ?? java_type" should be extraneous. Somewhere in generator uses it improperly. - var result = new Parameter (name, enum_type ?? java_type, enum_type ?? managed_type, enum_type != null, java_type, not_null); + var result = new Parameter (name, enum_type ?? java_type, enum_type ?? managed_type, enum_type != null, java_type, not_null) { + KotlinInlineClassJniType = kotlin_inline_jni, + }; if (elem.Attribute ("sender") != null) result.IsSender = true; SetLineInfo (result, elem, options); diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ClassGen.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ClassGen.cs index 83893e50a..3ec3fe818 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ClassGen.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ClassGen.cs @@ -298,6 +298,14 @@ public bool IsExplicitlyImplementedMethod (string sig) public bool IsFinal { get; set; } + // Kotlin @JvmInline value class support. + // When IsKotlinInlineClass is true, the generator emits a `readonly struct` + // wrapper around KotlinInlineClassUnderlyingJniType instead of the usual + // peer-class binding. + public bool IsKotlinInlineClass { get; set; } + + public string KotlinInlineClassUnderlyingJniType { get; set; } + public bool NeedsNew { get; set; } public string PeerConstructorPartialMethod { get; set; } diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs index d818df4c2..c22bec439 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs @@ -29,6 +29,7 @@ public Method (GenBase declaringType) : base (declaringType) public string JavaName { get; set; } public string ManagedOverride { get; set; } public string ManagedReturn { get; set; } + public string KotlinInlineClassReturnJniType { get; set; } public string PropertyNameOverride { get; set; } public string Return { get; set; } public bool ReturnNotNull { get; set; } @@ -153,6 +154,7 @@ public Method Clone (GenBase declaringType) clone.JavaName = JavaName; clone.ManagedOverride = ManagedOverride; clone.ManagedReturn = ManagedReturn; + clone.KotlinInlineClassReturnJniType = KotlinInlineClassReturnJniType; clone.PropertyNameOverride = PropertyNameOverride; clone.Return = Return; clone.ReturnNotNull = ReturnNotNull; diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Parameter.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Parameter.cs index 7bf81e1c0..658fa9c04 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Parameter.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Parameter.cs @@ -33,9 +33,17 @@ internal Parameter (string name, string type, string managedType, bool isEnumifi NotNull = notNull; } + // Kotlin @JvmInline value class JNI signature (e.g. "Lcom/example/MyColor;") + // for inline-class projection. When set, Validate replaces the managed type + // with the wrapper struct's full name so the C# parameter signature uses the + // struct, while JNI marshaling stays on the underlying primitive (the `type`). + public string KotlinInlineClassJniType { get; set; } + public Parameter Clone () { - return new Parameter (name, type, managed_type, is_enumified, rawtype, NotNull); + return new Parameter (name, type, managed_type, is_enumified, rawtype, NotNull) { + KotlinInlineClassJniType = KotlinInlineClassJniType, + }; } public string GetCall (CodeGenerationOptions opt) @@ -286,9 +294,28 @@ public bool Validate (CodeGenerationOptions opt, GenericParameterDefinitionList Report.LogCodedWarning (0, Report.WarningInvalidParameterType, this, type, context.GetContextTypeMember ()); return false; } + ApplyKotlinInlineClassProjection (opt, type_params); return true; } + // If KotlinInlineClassJniType is set, look up the wrapper ClassGen and + // override managed_type so the C# parameter type is the struct (e.g. + // "Com.Example.MyColor") while sym remains the underlying primitive + // for JNI marshaling. + void ApplyKotlinInlineClassProjection (CodeGenerationOptions opt, GenericParameterDefinitionList type_params) + { + if (string.IsNullOrEmpty (KotlinInlineClassJniType)) + return; + var javaName = TypeNameUtilities.JniSignatureToJavaTypeName (KotlinInlineClassJniType); + if (javaName == null) + return; + var wrapper = opt.SymbolTable.Lookup (javaName, type_params) as ClassGen; + if (wrapper == null || !wrapper.IsKotlinInlineClass) + return; + managed_type = wrapper.FullName; + NotNull = true; + } + public bool ShouldGenerateKeepAlive () { if (Symbol.IsEnum) diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ReturnValue.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ReturnValue.cs index 3f3bb13f4..ee7b9bb6c 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ReturnValue.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/ReturnValue.cs @@ -152,8 +152,27 @@ public bool Validate (CodeGenerationOptions opt, GenericParameterDefinitionList Report.LogCodedWarning (0, Report.WarningInvalidReturnType, owner, java_type, context.GetContextTypeMember ()); return false; } + ApplyKotlinInlineClassProjection (opt, type_params); return true; } + + // If the owning Method has KotlinInlineClassReturnJniType set, override + // managed_type with the wrapper struct's full name so the C# return type + // is the struct, while sym remains the underlying primitive for JNI. + void ApplyKotlinInlineClassProjection (CodeGenerationOptions opt, GenericParameterDefinitionList type_params) + { + var jni = owner?.KotlinInlineClassReturnJniType; + if (string.IsNullOrEmpty (jni)) + return; + var javaName = TypeNameUtilities.JniSignatureToJavaTypeName (jni); + if (javaName == null) + return; + var wrapper = opt.SymbolTable.Lookup (javaName, type_params) as ClassGen; + if (wrapper == null || !wrapper.IsKotlinInlineClass) + return; + managed_type = wrapper.FullName; + NotNull = true; + } } } diff --git a/tools/generator/SourceWriters/KotlinInlineClassStruct.cs b/tools/generator/SourceWriters/KotlinInlineClassStruct.cs new file mode 100644 index 000000000..a348f36fa --- /dev/null +++ b/tools/generator/SourceWriters/KotlinInlineClassStruct.cs @@ -0,0 +1,79 @@ +using System; +using MonoDroid.Generation; +using Xamarin.SourceWriter; + +namespace generator.SourceWriters +{ + // Emits a `readonly struct` wrapper for a Kotlin @JvmInline value class. + // The struct holds the underlying primitive (e.g. long, float) that the + // JVM passes across JNI, and provides implicit conversions so the wrapper + // can be used directly in projected method signatures while existing JNI + // thunks marshal the primitive value unchanged. + public class KotlinInlineClassStruct : TypeWriter + { + readonly ClassGen klass; + readonly string underlying_csharp_type; + + public KotlinInlineClassStruct (ClassGen klass, CodeGenerationOptions opt) + { + this.klass = klass; + underlying_csharp_type = JniPrimitiveToCSharpType (klass.KotlinInlineClassUnderlyingJniType); + + Name = klass.Name; + SetVisibility (klass.Visibility); + + klass.JavadocInfo?.AddJavadocs (Comments); + Comments.Add ($"// Metadata.xml XPath class reference: path=\"{klass.MetadataXPathReference}\""); + Comments.Add ("// Kotlin @JvmInline value class wrapper."); + } + + public override void WriteSignature (CodeWriter writer) + { + if (IsPublic) + writer.Write ("public "); + else if (IsInternal) + writer.Write ("internal "); + + writer.Write ("readonly partial struct "); + writer.Write (Name + " "); + writer.WriteLine (": global::System.IEquatable<" + Name + "> {"); + writer.Indent (); + } + + public override void WriteMembers (CodeWriter writer) + { + var t = underlying_csharp_type; + var n = Name; + + writer.WriteLine ($"public readonly {t} Value;"); + writer.WriteLine (); + writer.WriteLine ($"public {n} ({t} value) {{ Value = value; }}"); + writer.WriteLine (); + writer.WriteLine ($"public static implicit operator {t} ({n} value) => value.Value;"); + writer.WriteLine ($"public static implicit operator {n} ({t} value) => new {n} (value);"); + writer.WriteLine (); + writer.WriteLine ($"public static bool operator == ({n} left, {n} right) => left.Equals (right);"); + writer.WriteLine ($"public static bool operator != ({n} left, {n} right) => !left.Equals (right);"); + writer.WriteLine (); + writer.WriteLine ($"public bool Equals ({n} other) => Value.Equals (other.Value);"); + writer.WriteLine ($"public override bool Equals (object? obj) => obj is {n} other && Equals (other);"); + writer.WriteLine ("public override int GetHashCode () => Value.GetHashCode ();"); + writer.WriteLine ("public override string? ToString () => Value.ToString ();"); + } + + static string JniPrimitiveToCSharpType (string jni) + { + return jni switch { + "Z" => "bool", + "B" => "sbyte", + "C" => "char", + "D" => "double", + "F" => "float", + "I" => "int", + "J" => "long", + "S" => "short", + _ => "long", + }; + } + } +} diff --git a/tools/generator/Utilities/TypeNameUtilities.cs b/tools/generator/Utilities/TypeNameUtilities.cs index 9e9656d2f..67e73333d 100644 --- a/tools/generator/Utilities/TypeNameUtilities.cs +++ b/tools/generator/Utilities/TypeNameUtilities.cs @@ -9,6 +9,19 @@ namespace MonoDroid.Generation { public static class TypeNameUtilities { + // Convert a JNI type signature like "Lcom/example/MyColor;" into the Java + // type name "com.example.MyColor". Returns null for non-reference signatures + // (primitives or arrays) since those are not used by the Kotlin inline-class + // projection path. + public static string JniSignatureToJavaTypeName (string jniSignature) + { + if (string.IsNullOrEmpty (jniSignature)) + return null; + if (jniSignature.Length < 3 || jniSignature [0] != 'L' || jniSignature [jniSignature.Length - 1] != ';') + return null; + return jniSignature.Substring (1, jniSignature.Length - 2).Replace ('/', '.'); + } + // These must be sorted for BinarySearch to work // Missing "this" because it's handled elsewhere as "this_" internal static string [] reserved_keywords = new [] { From 03f56dd1823433233e24fdf9d7b0bcf52854eded Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 3 Jun 2026 11:26:19 -0500 Subject: [PATCH 3/6] Add real-Kotlin end-to-end generator test for inline classes Wires the four Kotlin .class files compiled by the kotlin-gradle/ fixture under tests/Xamarin.Android.Tools.Bytecode-Tests/ into a generator-level test that exercises the full Phase 2 pipeline: bytecode (KotlinFixups + XmlClassDeclarationBuilder) -> api.xml string -> XmlApiImporter.Parse + Validate -> JavaInteropCodeGenerator.WriteType and asserts the projected C# output emits readonly partial structs for MyColor/MyAlpha/MyDp, projects them in Widgets.tint/pad method signatures, and never falls back to peer-class bindings for them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../KotlinInlineClassEndToEndTests.cs | 152 ++++++++++++++++++ tests/generator-Tests/generator-Tests.csproj | 12 ++ 2 files changed, 164 insertions(+) create mode 100644 tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs diff --git a/tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs b/tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs new file mode 100644 index 000000000..6b31aa37d --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Java.Interop.Tools.Generator; +using MonoDroid.Generation; +using NUnit.Framework; +using Xamarin.Android.Binder; +using Xamarin.Android.Tools.Bytecode; + +namespace generatortests +{ + // dotnet/java-interop#1431 (Phase 2): end-to-end test that drives real + // Kotlin .class files (compiled by tests/Xamarin.Android.Tools.Bytecode-Tests/ + // kotlin-gradle/) through KotlinFixups -> XmlClassDeclarationBuilder -> + // XmlApiImporter -> JavaInteropCodeGenerator and asserts the projected C# + // output uses the generated `readonly partial struct` wrapper types in + // method signatures while keeping the JNI marshaling on the underlying + // primitive. + [TestFixture] + public class KotlinInlineClassEndToEndTests + { + [Test] + public void GeneratorProjectsRealKotlinInlineClassesAsStructs () + { + var apiXml = BuildApiXmlFromKotlinFixture (); + + // Sanity: api.xml carries the Phase 2 attributes class-parse emits. + StringAssert.Contains ("kotlin-inline-class=\"true\"", apiXml); + StringAssert.Contains ("kotlin-inline-class-jni-type=\"Lxat/bytecode/tests/MyColor;\"", apiXml); + StringAssert.Contains ("kotlin-inline-class-return-jni-type=\"Lxat/bytecode/tests/MyDp;\"", apiXml); + + var output = GenerateCSharp (apiXml, out var gens); + + // MyColor / MyAlpha / MyDp must be projected as readonly partial structs. + StringAssert.Contains ("readonly partial struct MyColor", output); + StringAssert.Contains ("readonly partial struct MyAlpha", output); + StringAssert.Contains ("readonly partial struct MyDp", output); + + // Underlying-primitive marshaling: MyColor/MyAlpha back J (long), + // MyDp backs F (float). + StringAssert.Contains ("public static implicit operator long (MyColor", output); + StringAssert.Contains ("public static implicit operator MyColor (long", output); + StringAssert.Contains ("public static implicit operator float (MyDp", output); + StringAssert.Contains ("public static implicit operator MyDp (float", output); + + // Widgets.tint(MyColor) / tint(MyAlpha) / tint(MyDp) overloads must + // project the inline-class param in the C# signature. Phase 1 + // (#1432) appends a JVM-name-mangle hash suffix to disambiguate. + StringAssert.Contains ("Tint_Rn_QMJI (Xat.Bytecode.Tests.MyColor color)", output); + StringAssert.Contains ("Tint_uzYZ1wI (Xat.Bytecode.Tests.MyAlpha alpha)", output); + StringAssert.Contains ("Tint_L3D9Hvg (Xat.Bytecode.Tests.MyDp dp)", output); + + // Widgets.pad(MyDp): MyDp -> the return type uses MyDp. + StringAssert.Contains ("Xat.Bytecode.Tests.MyDp Pad_D3yWXm0 (Xat.Bytecode.Tests.MyDp dp)", output); + StringAssert.Contains ("Xat.Bytecode.Tests.MyDp Pad_Puo4k8A (Xat.Bytecode.Tests.MyDp dp1, Xat.Bytecode.Tests.MyDp dp2)", output); + + // And no `Java.Lang.Object`-derived peer class for the inline classes + // (the wrapper struct fully replaces the peer-class binding). + StringAssert.DoesNotContain ("public partial class MyColor", output); + StringAssert.DoesNotContain ("public partial class MyAlpha", output); + StringAssert.DoesNotContain ("public partial class MyDp", output); + + // All three inline classes survived as ClassGen entries with the + // IsKotlinInlineClass flag set. + Assert.IsTrue (gens.OfType ().Count (g => g.IsKotlinInlineClass) >= 3, + $"Expected at least 3 inline-class ClassGens, generator output was:\n{output}"); + } + + // Run the four kotlin-gradle .class files through KotlinFixups and + // XmlClassDeclarationBuilder to produce the same api.xml the generator + // would normally consume off disk. + static string BuildApiXmlFromKotlinFixture () + { + var classes = new List { + LoadClassFile ("MyColor.class"), + LoadClassFile ("MyAlpha.class"), + LoadClassFile ("MyDp.class"), + LoadClassFile ("Widgets.class"), + }; + KotlinFixups.Fixup (classes); + + var classPath = new ClassPath { ApiSource = "class-parse" }; + foreach (var c in classes) + classPath.Add (c); + + var sw = new StringWriter (); + classPath.SaveXmlDescription (sw); + var xml = sw.ToString (); + + // XmlApiImporter needs java.lang.Object in the symbol table so the + // generated peer types (and their RetVal/Parameter symbols) resolve + // during Validate. Splice a minimal stub package into the api root. + var doc = XDocument.Parse (xml); + doc.Root.AddFirst (XElement.Parse ( + "" + + "" + + "")); + return doc.ToString (); + } + + static ClassFile LoadClassFile (string resource) + { + var assembly = typeof (KotlinInlineClassEndToEndTests).Assembly; + var name = assembly.GetManifestResourceNames () + .FirstOrDefault (n => n.EndsWith ("." + resource, System.StringComparison.OrdinalIgnoreCase)) + ?? throw new FileNotFoundException ($"Embedded resource '{resource}' not found."); + using (var stream = assembly.GetManifestResourceStream (name)) + return new ClassFile (stream); + } + + // Drive the parsed api.xml through XmlApiImporter + Validate + the + // JavaInteropCodeGenerator to produce the C# binding text. + static string GenerateCSharp (string apiXml, out List gens) + { + var options = new CodeGenerationOptions { + CodeGenerationTarget = CodeGenerationTarget.JavaInterop1, + }; + var sb = new System.Text.StringBuilder (); + var writer = new StringWriter (sb); + var generator = options.CreateCodeGenerator (writer); + + var doc = XDocument.Parse (apiXml); + gens = XmlApiImporter.Parse (doc, options); + + foreach (var gen in gens) + options.SymbolTable.AddType (gen); + + foreach (var gen in gens) + gen.FixupAccessModifiers (options); + + foreach (var gen in gens) + gen.Validate (options, new GenericParameterDefinitionList (), generator.Context); + + foreach (var gen in gens) + gen.FillProperties (); + + foreach (var gen in gens) + gen.FixupMethodOverrides (options); + + var info = new GenerationInfo ("", "", "MyAssembly"); + foreach (var gen in gens) { + generator.Context.ContextTypes.Push (gen); + generator.WriteType (gen, string.Empty, info); + generator.Context.ContextTypes.Pop (); + } + + return sb.ToString (); + } + } +} diff --git a/tests/generator-Tests/generator-Tests.csproj b/tests/generator-Tests/generator-Tests.csproj index 39d09c875..ba9ac900c 100644 --- a/tests/generator-Tests/generator-Tests.csproj +++ b/tests/generator-Tests/generator-Tests.csproj @@ -26,6 +26,18 @@ + + + + + + + + + From 4013ded1c3597ac5174e32965fcabec120c29ef2 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 3 Jun 2026 11:32:54 -0500 Subject: [PATCH 4/6] Unmangle Kotlin inline-class JVM method names When the Kotlin compiler mangles a JVM method name for inline-class binary compatibility (e.g. `tint(MyColor)` -> `tint-Rn_QMJI`), recover the unmangled Kotlin source name and surface it in api.xml as the `managedName` attribute. The mangled JVM name still appears in `name`/`jni-signature` so JNI invocation targets the actual method. With this and the Phase 2 inline-class -> struct projection, methods that erase to colliding JVM signatures emit as plain C# overloads distinguished by struct type, e.g.: void Tint (MyColor color); void Tint (MyAlpha alpha); void Tint (MyDp dp); instead of the previous `Tint_Rn_QMJI` / `Tint_uzYZ1wI` mangled names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Kotlin/KotlinFixups.cs | 16 ++++++++++++++ src/Xamarin.Android.Tools.Bytecode/Methods.cs | 9 ++++++++ .../XmlClassDeclarationBuilder.cs | 13 ++++++++++++ .../KotlinInlineClassEndToEndTests.cs | 21 ++++++++++++------- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs b/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs index 8cce2c98b..720aadb16 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs +++ b/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs @@ -299,6 +299,22 @@ static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinCl // Same projection as above, but for the return type. method.KotlinInlineClassReturnJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, inlineClasses); + + // Recover the unmangled Kotlin source-level name when the Kotlin + // compiler mangled the JVM name for inline-class binary compat + // (e.g. JVM name `tint-Rn_QMJI`, Kotlin name `tint`). The generator + // will emit this as the C# binding name (PascalCased to match + // `managedName` conventions); the JVM name stays the JNI invocation + // target. See dotnet/java-interop#1431 (Phase 2). + if (metadata.Name != null && metadata.JvmName != null && metadata.Name != metadata.JvmName) + method.KotlinName = PascalCase (metadata.Name); + } + + static string PascalCase (string name) + { + if (string.IsNullOrEmpty (name) || char.IsUpper (name [0])) + return name; + return char.ToUpperInvariant (name [0]) + name.Substring (1); } public static (int start, int end) CreateParameterMap (MethodInfo method, KotlinFunction function, KotlinClass? kotlinClass) diff --git a/src/Xamarin.Android.Tools.Bytecode/Methods.cs b/src/Xamarin.Android.Tools.Bytecode/Methods.cs index e157fc72a..753a90f33 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Methods.cs +++ b/src/Xamarin.Android.Tools.Bytecode/Methods.cs @@ -43,6 +43,15 @@ public sealed class MethodInfo { // See dotnet/java-interop#1431 (Phase 2). public string? KotlinInlineClassReturnJniType { get; set; } + // Unmangled Kotlin source-level method name when it differs from the + // JVM-level `Name`. Populated for methods that the Kotlin compiler + // mangles for inline-class binary compatibility (e.g. JVM name + // `tint-Rn_QMJI`, Kotlin name `tint`). Surfaced into api.xml as + // `managedName` so the generator emits a clean C# overload while the + // JVM `Name` stays in `name`/`jni-signature` for native invocation. + // See dotnet/java-interop#1431 (Phase 2). + public string? KotlinName { get; set; } + public MethodInfo (ConstantPool constantPool, ClassFile declaringType, Stream stream) { ConstantPool = constantPool; diff --git a/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs b/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs index ffe0bed21..765278255 100644 --- a/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs +++ b/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs @@ -356,6 +356,7 @@ XElement GetMethod (string element, string name, MethodInfo method, string? retu new XAttribute ("deprecated", GetDeprecatedValue (method.Attributes)), new XAttribute ("final", (method.AccessFlags & MethodAccessFlags.Final) != 0), new XAttribute ("name", name), + GetManagedName (method), GetNative (method), ret, jniRet, @@ -373,6 +374,18 @@ XElement GetMethod (string element, string name, MethodInfo method, string? retu GetExceptions (method)); } + // dotnet/java-interop#1431 (Phase 2): when the Kotlin compiler mangled + // the JVM method name for inline-class binary compatibility (e.g. + // `tint-Rn_QMJI`), expose the unmangled Kotlin source name via + // `managedName` so the generator emits a clean C# overload. The JVM + // name remains in `name`/`jni-signature` for JNI invocation. + static XAttribute? GetManagedName (MethodInfo method) + { + if (string.IsNullOrEmpty (method.KotlinName)) + return null; + return new XAttribute ("managedName", method.KotlinName!); + } + // dotnet/java-interop#1431 (Phase 2): when a method's Kotlin source-level // return type was a `@JvmInline value class`, surface that type's JNI // signature so the generator can project the return type to a wrapper struct. diff --git a/tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs b/tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs index 6b31aa37d..9913b7a20 100644 --- a/tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs +++ b/tests/generator-Tests/Unit-Tests/KotlinInlineClassEndToEndTests.cs @@ -46,15 +46,22 @@ public void GeneratorProjectsRealKotlinInlineClassesAsStructs () StringAssert.Contains ("public static implicit operator MyDp (float", output); // Widgets.tint(MyColor) / tint(MyAlpha) / tint(MyDp) overloads must - // project the inline-class param in the C# signature. Phase 1 - // (#1432) appends a JVM-name-mangle hash suffix to disambiguate. - StringAssert.Contains ("Tint_Rn_QMJI (Xat.Bytecode.Tests.MyColor color)", output); - StringAssert.Contains ("Tint_uzYZ1wI (Xat.Bytecode.Tests.MyAlpha alpha)", output); - StringAssert.Contains ("Tint_L3D9Hvg (Xat.Bytecode.Tests.MyDp dp)", output); + // project the inline-class param in the C# signature. The Kotlin + // compiler mangles the JVM names for inline-class binary compat + // (e.g. `tint-Rn_QMJI`); we recover the unmangled name so they + // emit as plain C# overloads distinguished by struct type. + StringAssert.Contains ("Tint (Xat.Bytecode.Tests.MyColor color)", output); + StringAssert.Contains ("Tint (Xat.Bytecode.Tests.MyAlpha alpha)", output); + StringAssert.Contains ("Tint (Xat.Bytecode.Tests.MyDp dp)", output); // Widgets.pad(MyDp): MyDp -> the return type uses MyDp. - StringAssert.Contains ("Xat.Bytecode.Tests.MyDp Pad_D3yWXm0 (Xat.Bytecode.Tests.MyDp dp)", output); - StringAssert.Contains ("Xat.Bytecode.Tests.MyDp Pad_Puo4k8A (Xat.Bytecode.Tests.MyDp dp1, Xat.Bytecode.Tests.MyDp dp2)", output); + StringAssert.Contains ("Xat.Bytecode.Tests.MyDp Pad (Xat.Bytecode.Tests.MyDp dp)", output); + StringAssert.Contains ("Xat.Bytecode.Tests.MyDp Pad (Xat.Bytecode.Tests.MyDp dp1, Xat.Bytecode.Tests.MyDp dp2)", output); + + // And the JVM-mangled hash-suffix names must NOT leak into the + // generated C# (regression guard for the unmangling path). + StringAssert.DoesNotContain ("Tint_", output); + StringAssert.DoesNotContain ("Pad_", output); // And no `Java.Lang.Object`-derived peer class for the inline classes // (the wrapper struct fully replaces the peer-class binding). From 2eb68f53c7c312b2a9bbffc5170d547aaf85c67e Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 3 Jun 2026 11:47:03 -0500 Subject: [PATCH 5/6] Address PR #1440 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. KotlinInlineClassStruct.cs: drop unused `using System;` and unused `klass` field; gate `object?`/`string?` annotations on `opt.NullableOperator` so consumers without nullable enabled don't get CS8632 warnings. 2. TypeNameUtilities.JniSignatureToJavaTypeName: also translate `\$` (JNI nested-type separator) to `.` so SymbolTable.Lookup() resolves nested inline classes. 3. KotlinFixups.DetectInlineClasses: only stamp underlying-JNI type when there is exactly one non-synthetic instance field AND that field is a JVM primitive descriptor (Z/B/C/D/F/I/J/S). Reference- backed inline classes (e.g. `value class Tag(val s: String)`) are skipped — the wrapper struct currently emits a primitive `Value` field, so they would otherwise produce wrong bindings. 4. KotlinFixups.GetInlineClassJniType: now also takes the JVM-erased descriptor of the position being projected (param/return/property) and only returns a JNI-type when it equals the inline class's underlying primitive. Boxed / nullable / generic inline-class positions (where the JVM signature stays `L...;`) are no longer incorrectly stamped — they fall through to the legacy peer-class binding path so JNI marshaling stays consistent. Applied at all four stamping sites: function param, function return, property getter return, property setter param. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Kotlin/KotlinFixups.cs | 74 ++++++++++++++----- .../SourceWriters/KotlinInlineClassStruct.cs | 10 +-- .../generator/Utilities/TypeNameUtilities.cs | 4 +- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs b/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs index 720aadb16..4bc7c057a 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs +++ b/src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs @@ -112,11 +112,24 @@ static Dictionary DetectInlineClasses (IList classes) // The single non-synthetic, non-static instance field is the // inline-class backing value. (Synthetic fields like `Companion` - // are filtered out.) - var backing = c.Fields.FirstOrDefault (f => + // are filtered out.) We additionally require: + // - exactly one such field exists (Kotlin inline classes have + // a single property; multiple non-synthetic instance fields + // means something else is going on and we shouldn't trust + // this as the backing field). + // - the field is a JVM *primitive* descriptor — the wrapper + // struct currently emits the underlying as a primitive + // C# type, so reference-backed inline classes (e.g. + // `value class Tag(val s: String)`) would produce wrong + // bindings. Skip these for now; they fall back to the + // standard peer-class binding path. + var instance_fields = c.Fields.Where (f => !f.AccessFlags.HasFlag (FieldAccessFlags.Synthetic) && - !f.AccessFlags.HasFlag (FieldAccessFlags.Static)); - if (backing is null) + !f.AccessFlags.HasFlag (FieldAccessFlags.Static)).ToList (); + if (instance_fields.Count != 1) + continue; + var backing = instance_fields [0]; + if (!IsJvmPrimitiveDescriptor (backing.Descriptor)) continue; c.KotlinInlineClassUnderlyingJniType = backing.Descriptor; @@ -135,18 +148,42 @@ static Dictionary DetectInlineClasses (IList classes) } // JNI signature for the Kotlin inline class referenced by `kotlinTypeClassName`, - // or null when the name is unknown / not an inline class. The returned form - // has a leading `L` and trailing `;` so it matches `ClassFile.FullJniName` + // or null when projection should not apply. The returned form has a + // leading `L` and trailing `;` so it matches `ClassFile.FullJniName` // and other JNI-signature strings used throughout the pipeline. - static string? GetInlineClassJniType (string? kotlinTypeClassName, IDictionary inlineClasses) + // + // `jvmDescriptor` is the *JVM-erased* descriptor of the actual position + // (parameter / return / property) we're considering. We only project + // when it equals the inline class's underlying primitive: that's the + // case where Kotlin truly erased to the primitive and our wrapper + // struct's `implicit operator ` makes JNI marshaling work + // transparently. Boxed / nullable / generic positions keep their JVM + // reference signature (`L...MyColor;` or `Ljava/lang/Object;`); for + // those, projecting to a struct would mismatch JNI marshaling, so we + // fall through and let them keep the legacy peer-class binding path. + static string? GetInlineClassJniType (string? kotlinTypeClassName, string? jvmDescriptor, IDictionary inlineClasses) { - if (kotlinTypeClassName is null) + if (kotlinTypeClassName is null || jvmDescriptor is null) + return null; + if (!inlineClasses.TryGetValue (kotlinTypeClassName, out var underlying)) return null; - if (!inlineClasses.ContainsKey (kotlinTypeClassName)) + if (jvmDescriptor != underlying) return null; return "L" + kotlinTypeClassName; } + // Returns true for JVM primitive descriptors (Z/B/C/D/F/I/J/S). Excludes + // `V` (void), reference (`L...;`), and array (`[...`) descriptors. + static bool IsJvmPrimitiveDescriptor (string? descriptor) + { + if (descriptor is null || descriptor.Length != 1) + return false; + return descriptor [0] switch { + 'Z' or 'B' or 'C' or 'D' or 'F' or 'I' or 'J' or 'S' => true, + _ => false, + }; + } + static void FixupClassVisibility (ClassFile klass, KotlinClass metadata) { // Hide class if it isn't Public/Protected @@ -287,18 +324,21 @@ static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinCl java_p.KotlinType = GetKotlinType (java_p.Type.TypeSignature, kotlin_p.Type.ClassName); // Inline-class projection: if the Kotlin source-level type for this - // parameter is a `@JvmInline value class` we know about, record its - // JNI signature so the generator can later swap the parameter type - // for a strongly-typed wrapper struct while keeping JNI marshaling - // on the underlying primitive. See dotnet/java-interop#1431 (Phase 2). - java_p.KotlinInlineClassJniType = GetInlineClassJniType (kotlin_p.Type.ClassName, inlineClasses); + // parameter is a `@JvmInline value class` we know about AND the + // JVM-erased parameter descriptor is the inline class's + // underlying primitive, record its JNI signature so the + // generator can later swap the parameter type for a strongly- + // typed wrapper struct while keeping JNI marshaling on the + // underlying primitive. Boxed positions are skipped. + // See dotnet/java-interop#1431 (Phase 2). + java_p.KotlinInlineClassJniType = GetInlineClassJniType (kotlin_p.Type.ClassName, java_p.Type.TypeSignature, inlineClasses); } // Handle erasure of Kotlin unsigned types method.KotlinReturnType = GetKotlinType (method.ReturnType.TypeSignature, metadata.ReturnType?.ClassName); // Same projection as above, but for the return type. - method.KotlinInlineClassReturnJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, inlineClasses); + method.KotlinInlineClassReturnJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, method.ReturnType.TypeSignature, inlineClasses); // Recover the unmangled Kotlin source-level name when the Kotlin // compiler mangled the JVM name for inline-class binary compat @@ -393,7 +433,7 @@ static void FixupProperty (MethodInfo? getter, MethodInfo? setter, KotlinPropert // Handle erasure of Kotlin unsigned types if (getter != null) { getter.KotlinReturnType = GetKotlinType (getter.ReturnType.TypeSignature, metadata.ReturnType?.ClassName); - getter.KotlinInlineClassReturnJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, inlineClasses); + getter.KotlinInlineClassReturnJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, getter.ReturnType.TypeSignature, inlineClasses); } if (setter != null) { @@ -406,7 +446,7 @@ static void FixupProperty (MethodInfo? getter, MethodInfo? setter, KotlinPropert // Handle erasure of Kotlin unsigned types setter_parameter.KotlinType = GetKotlinType (setter_parameter.Type.TypeSignature, metadata.ReturnType?.ClassName); - setter_parameter.KotlinInlineClassJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, inlineClasses); + setter_parameter.KotlinInlineClassJniType = GetInlineClassJniType (metadata.ReturnType?.ClassName, setter_parameter.Type.TypeSignature, inlineClasses); } } diff --git a/tools/generator/SourceWriters/KotlinInlineClassStruct.cs b/tools/generator/SourceWriters/KotlinInlineClassStruct.cs index a348f36fa..f4738e6c9 100644 --- a/tools/generator/SourceWriters/KotlinInlineClassStruct.cs +++ b/tools/generator/SourceWriters/KotlinInlineClassStruct.cs @@ -1,4 +1,3 @@ -using System; using MonoDroid.Generation; using Xamarin.SourceWriter; @@ -11,13 +10,13 @@ namespace generator.SourceWriters // thunks marshal the primitive value unchanged. public class KotlinInlineClassStruct : TypeWriter { - readonly ClassGen klass; readonly string underlying_csharp_type; + readonly string nullable; public KotlinInlineClassStruct (ClassGen klass, CodeGenerationOptions opt) { - this.klass = klass; underlying_csharp_type = JniPrimitiveToCSharpType (klass.KotlinInlineClassUnderlyingJniType); + nullable = opt.NullableOperator; Name = klass.Name; SetVisibility (klass.Visibility); @@ -44,6 +43,7 @@ public override void WriteMembers (CodeWriter writer) { var t = underlying_csharp_type; var n = Name; + var q = nullable; writer.WriteLine ($"public readonly {t} Value;"); writer.WriteLine (); @@ -56,9 +56,9 @@ public override void WriteMembers (CodeWriter writer) writer.WriteLine ($"public static bool operator != ({n} left, {n} right) => !left.Equals (right);"); writer.WriteLine (); writer.WriteLine ($"public bool Equals ({n} other) => Value.Equals (other.Value);"); - writer.WriteLine ($"public override bool Equals (object? obj) => obj is {n} other && Equals (other);"); + writer.WriteLine ($"public override bool Equals (object{q} obj) => obj is {n} other && Equals (other);"); writer.WriteLine ("public override int GetHashCode () => Value.GetHashCode ();"); - writer.WriteLine ("public override string? ToString () => Value.ToString ();"); + writer.WriteLine ($"public override string{q} ToString () => Value.ToString ();"); } static string JniPrimitiveToCSharpType (string jni) diff --git a/tools/generator/Utilities/TypeNameUtilities.cs b/tools/generator/Utilities/TypeNameUtilities.cs index 67e73333d..a7db40fd2 100644 --- a/tools/generator/Utilities/TypeNameUtilities.cs +++ b/tools/generator/Utilities/TypeNameUtilities.cs @@ -19,7 +19,9 @@ public static string JniSignatureToJavaTypeName (string jniSignature) return null; if (jniSignature.Length < 3 || jniSignature [0] != 'L' || jniSignature [jniSignature.Length - 1] != ';') return null; - return jniSignature.Substring (1, jniSignature.Length - 2).Replace ('/', '.'); + return jniSignature.Substring (1, jniSignature.Length - 2) + .Replace ('/', '.') + .Replace ('$', '.'); } // These must be sorted for BinarySearch to work From 48f9f668aea17dbe56bb25694d62e73d8ab0b87a Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 3 Jun 2026 12:30:11 -0500 Subject: [PATCH 6/6] Address PR #1440 second review pass - XmlClassDeclarationBuilder: drop null-forgiving '!' (4 sites). The string.IsNullOrEmpty guard's [NotNullWhen(false)] annotation already narrows the type, so the operator was unnecessary and violated the repo convention banning '!'. - JavaInteropCodeGenerator.WriteType: collapse the 'klass'/'klass2' duplicate pattern variables into a nested if-else inside a single ClassGen match. - KotlinInlineClassStruct.JniPrimitiveToCSharpType: replace the silent '_ => \"long\"' fallback with ArgumentOutOfRangeException. DetectInlineClasses already filters to primitive descriptors, so this branch is unreachable for valid input - failing fast prevents a non-primitive from silently producing a wrong 'long' wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../XmlClassDeclarationBuilder.cs | 8 ++++---- .../JavaInteropCodeGenerator.cs | 11 ++++++----- .../SourceWriters/KotlinInlineClassStruct.cs | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs b/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs index 765278255..6ec53e39b 100644 --- a/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs +++ b/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs @@ -59,7 +59,7 @@ IEnumerable GetKotlinInlineClassAttributes () if (string.IsNullOrEmpty (underlying)) yield break; yield return new XAttribute ("kotlin-inline-class", "true"); - yield return new XAttribute ("kotlin-inline-class-underlying-jni-type", underlying!); + yield return new XAttribute ("kotlin-inline-class-underlying-jni-type", underlying); } string GetElementName () @@ -383,7 +383,7 @@ XElement GetMethod (string element, string name, MethodInfo method, string? retu { if (string.IsNullOrEmpty (method.KotlinName)) return null; - return new XAttribute ("managedName", method.KotlinName!); + return new XAttribute ("managedName", method.KotlinName); } // dotnet/java-interop#1431 (Phase 2): when a method's Kotlin source-level @@ -393,7 +393,7 @@ XElement GetMethod (string element, string name, MethodInfo method, string? retu { if (string.IsNullOrEmpty (method.KotlinInlineClassReturnJniType)) return null; - return new XAttribute ("kotlin-inline-class-return-jni-type", method.KotlinInlineClassReturnJniType!); + return new XAttribute ("kotlin-inline-class-return-jni-type", method.KotlinInlineClassReturnJniType); } static XAttribute? GetNative (MethodInfo method) @@ -464,7 +464,7 @@ IEnumerable GetMethodParameters (MethodInfo method) { if (string.IsNullOrEmpty (p.KotlinInlineClassJniType)) return null; - return new XAttribute ("kotlin-inline-class-jni-type", p.KotlinInlineClassJniType!); + return new XAttribute ("kotlin-inline-class-jni-type", p.KotlinInlineClassJniType); } IEnumerable GetExceptions (MethodInfo method) diff --git a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs index 8cb5770cc..e820f4d40 100644 --- a/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs +++ b/tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs @@ -42,11 +42,12 @@ public virtual void WriteType (GenBase gen, string indent, GenerationInfo gen_in if (gen is InterfaceGen iface) type_writer = new BoundInterface (iface, opt, Context, gen_info); - else if (gen is ClassGen klass && klass.IsKotlinInlineClass) - type_writer = new KotlinInlineClassStruct (klass, opt); - else if (gen is ClassGen klass2) - type_writer = new BoundClass (klass2, opt, Context, gen_info); - else + else if (gen is ClassGen klass) { + if (klass.IsKotlinInlineClass) + type_writer = new KotlinInlineClassStruct (klass, opt); + else + type_writer = new BoundClass (klass, opt, Context, gen_info); + } else throw new InvalidOperationException ("Unknown GenBase type"); // We do this here because we only want to check for top-level types, diff --git a/tools/generator/SourceWriters/KotlinInlineClassStruct.cs b/tools/generator/SourceWriters/KotlinInlineClassStruct.cs index f4738e6c9..912070e4e 100644 --- a/tools/generator/SourceWriters/KotlinInlineClassStruct.cs +++ b/tools/generator/SourceWriters/KotlinInlineClassStruct.cs @@ -1,3 +1,4 @@ +using System; using MonoDroid.Generation; using Xamarin.SourceWriter; @@ -72,7 +73,7 @@ static string JniPrimitiveToCSharpType (string jni) "I" => "int", "J" => "long", "S" => "short", - _ => "long", + _ => throw new ArgumentOutOfRangeException (nameof (jni), jni, "Unsupported JNI primitive descriptor"), }; } }