Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
155 changes: 150 additions & 5 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);

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);
}

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
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
48 changes: 48 additions & 0 deletions src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (),
Expand All @@ -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<XAttribute> 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)
Expand Down Expand Up @@ -343,9 +356,11 @@ 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,
GetKotlinInlineClassReturnJniType (method),
new XAttribute ("static", (method.AccessFlags & MethodAccessFlags.Static) != 0),
GetSynchronized (method),
new XAttribute ("visibility", GetVisibility (method.AccessFlags)),
Expand All @@ -359,6 +374,28 @@ 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.
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)
Expand Down Expand Up @@ -415,10 +452,21 @@ IEnumerable<XElement> 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<XElement> GetExceptions (MethodInfo method)
{
foreach (var t in method.GetThrows ()) {
Expand Down
Loading