Skip to content
Open
6 changes: 6 additions & 0 deletions src/Xamarin.Android.Tools.Bytecode/ClassFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand Down
181 changes: 169 additions & 12 deletions src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ public static class KotlinFixups
{
public static void Fixup (IList<ClassFile> 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<RuntimeVisibleAnnotationsAttribute> ().FirstOrDefault ();
Expand Down Expand Up @@ -51,15 +58,15 @@ public static void Fixup (IList<ClassFile> 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) {
foreach (var prop in metadata.Properties) {
var getter = FindJavaPropertyGetter (metadata, prop, c);
var setter = FindJavaPropertySetter (metadata, prop, c);
var getter = FindJavaPropertyGetter (metadata, prop, c, inlineClasses);
var setter = FindJavaPropertySetter (metadata, prop, c, inlineClasses);

FixupProperty (getter, setter, prop);
FixupProperty (getter, setter, prop, inlineClasses);

FixupField (FindJavaFieldProperty (metadata, prop, c), prop);
}
Expand All @@ -71,6 +78,112 @@ public static void Fixup (IList<ClassFile> 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<string, string> DetectInlineClasses (IList<ClassFile> classes)
{
var map = new Dictionary<string, string> (StringComparer.Ordinal);
foreach (var c in classes) {
var ann = c.Attributes.OfType<RuntimeVisibleAnnotationsAttribute> ().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.) 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)).ToList ();
if (instance_fields.Count != 1)
continue;
var backing = instance_fields [0];
if (!IsJvmPrimitiveDescriptor (backing.Descriptor))
continue;

c.KotlinInlineClassUnderlyingJniType = backing.Descriptor;

Comment thread
jonathanpeppers marked this conversation as resolved.
// 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 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.
//
// `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 <primitive>` 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<string, string> inlineClasses)
{
if (kotlinTypeClassName is null || jvmDescriptor is null)
return null;
if (!inlineClasses.TryGetValue (kotlinTypeClassName, out var underlying))
return null;
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
Expand Down Expand Up @@ -179,7 +292,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<string, string> inlineClasses)
{
if (method is null || !method.IsPubliclyVisible)
return;
Expand Down Expand Up @@ -209,10 +322,39 @@ 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 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, method.ReturnType.TypeSignature, 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);
Comment thread
jonathanpeppers marked this conversation as resolved.
}

public static (int start, int end) CreateParameterMap (MethodInfo method, KotlinFunction function, KotlinClass? kotlinClass)
Expand Down Expand Up @@ -267,7 +409,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<string, string> inlineClasses)
{
if (getter is null && setter is null)
return;
Expand All @@ -289,8 +431,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, getter.ReturnType.TypeSignature, inlineClasses);
}
Comment thread
jonathanpeppers marked this conversation as resolved.

if (setter != null) {
var setter_parameter = setter.GetParameters ().First ();
Expand All @@ -302,6 +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, setter_parameter.Type.TypeSignature, inlineClasses);
}
Comment thread
jonathanpeppers marked this conversation as resolved.
}

Expand Down Expand Up @@ -382,7 +527,7 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
return possible_methods.FirstOrDefault ();
}

static MethodInfo? FindJavaPropertyGetter (KotlinFile kotlinClass, KotlinProperty property, ClassFile klass)
static MethodInfo? FindJavaPropertyGetter (KotlinFile kotlinClass, KotlinProperty property, ClassFile klass, IDictionary<string, string> inlineClasses)
{
// Private properties do not have getters
if (property.IsPrivateVisibility)
Expand All @@ -399,12 +544,12 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
possible_methods = possible_methods.Where (method =>
method.GetParameters ().Length == 0 &&
property.ReturnType != null &&
TypesMatch (method.ReturnType, property.ReturnType, kotlinClass));
TypesMatch (method.ReturnType, property.ReturnType, kotlinClass, inlineClasses));

return possible_methods.FirstOrDefault ();
}

static MethodInfo? FindJavaPropertySetter (KotlinFile kotlinClass, KotlinProperty property, ClassFile klass)
static MethodInfo? FindJavaPropertySetter (KotlinFile kotlinClass, KotlinProperty property, ClassFile klass, IDictionary<string, string> inlineClasses)
{
// Private properties do not have setters
if (property.IsPrivateVisibility)
Expand All @@ -422,7 +567,7 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
property.ReturnType != null &&
method.GetParameters ().Length == 1 &&
method.ReturnType.BinaryName == "V" &&
TypesMatch (method.GetParameters () [0].Type, property.ReturnType, kotlinClass));
TypesMatch (method.GetParameters () [0].Type, property.ReturnType, kotlinClass, inlineClasses));

return possible_methods.FirstOrDefault ();
}
Expand All @@ -445,7 +590,7 @@ static bool ParametersMatch (KotlinFile kotlinClass, MethodInfo method, List<Kot
return true;
}

static bool TypesMatch (TypeInfo javaType, KotlinType kotlinType, KotlinFile? kotlinFile)
static bool TypesMatch (TypeInfo javaType, KotlinType kotlinType, KotlinFile? kotlinFile, IDictionary<string, string>? inlineClasses = null)
{
// Generic type
if (!string.IsNullOrWhiteSpace (kotlinType.TypeParameterName) && $"T{kotlinType.TypeParameterName};" == javaType.TypeSignature)
Expand All @@ -458,6 +603,18 @@ static bool TypesMatch (TypeInfo javaType, KotlinType kotlinType, KotlinFile? ko
if (javaType.BinaryName == "Ljava/lang/Object;")
return true;

// dotnet/java-interop#1431 (Phase 2): the JVM erases @JvmInline value
// class types to their underlying primitive descriptor, so e.g. a
// `MyColor` property (ULong-backed) appears in the bytecode as `()J`
// even though the Kotlin metadata still says `MyColor`. Accept the
// match when the JVM primitive matches the inline class's recorded
// underlying-primitive descriptor.
if (inlineClasses != null && IsJvmPrimitiveDescriptor (javaType.BinaryName) &&
kotlinType.ClassName != null &&
inlineClasses.TryGetValue (kotlinType.ClassName, out var underlying) &&
underlying == javaType.BinaryName)
return true;

// Sometimes Kotlin keeps its native types rather than converting them to Java native types
// ie: "Lkotlin/UShort;" instead of "S"
if (javaType.BinaryName.StartsWith ("L", StringComparison.Ordinal) && javaType.BinaryName.EndsWith (";", StringComparison.Ordinal)) {
Expand Down
25 changes: 25 additions & 0 deletions src/Xamarin.Android.Tools.Bytecode/Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ 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; }

// 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;
Expand Down Expand Up @@ -378,6 +395,14 @@ public sealed class ParameterInfo : IEquatable<ParameterInfo> {
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)
Expand Down
Loading