Skip to content

Commit b7caa78

Browse files
authored
[Byecode] Hide internal Kotlin fields marked with @JvmField (#1004)
Fixes: #954 Context: https://github.com/Kotlin/kotlinx.coroutines/blob/f0874d1c5ce1e5c8f091be2981bea3b80168c14e/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt#L71-L75 When binding `org.jetbrains.kotlinx.kotlinx-coroutines-core-jvm`, the following Kotlin-internal declarations: @JvmField internal val NonBlockingContext: TaskContext = TaskContextImpl(TASK_NON_BLOCKING) @JvmField internal val BlockingContext: TaskContext = TaskContextImpl(TASK_PROBABLY_BLOCKING) are not being marked as private in the Java API. This causes binding errors because their type (`TaskContext`) is a Kotlin-internal interface which is removed from the Java API. Internal Kotlin fields that are marked with [`@JvmField`][0] are exposed as `public` in the compiled Bytecode so they can be consumed by Java. // Kotlin @JvmField internal val city: String = "London" // Java @JvmField @NotNull public final String city = "London"; This is a bit of a conundrum because Kotlin `internal` means "can't be consumed by other libraries" while `@JvmField` means "make this consumable by other Java libraries". 🤷‍♂️ Let's honor the original Kotlin `internal` visibility to produce consistent bindings. (The `kotlin-internal` visibility can be overridden with `generator` metadata if desired.) We need to examine the encoded Kotlin metadata to determine if the field should be marked as `kotlin-internal` the same way we do with methods and properties. [0]: https://kotlinlang.org/docs/java-to-kotlin-interop.html#instance-fields
1 parent 22d5687 commit b7caa78

File tree

7 files changed

+57
-5
lines changed

7 files changed

+57
-5
lines changed

src/Xamarin.Android.Tools.Bytecode/ClassFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public ClassFile (Stream stream)
4343
thisClass = stream.ReadNetworkUInt16 ();
4444
superClass = stream.ReadNetworkUInt16 ();
4545
Interfaces = new Interfaces (ConstantPool, stream);
46-
Fields = new Fields (ConstantPool, stream);
46+
Fields = new Fields (ConstantPool, this, stream);
4747
Methods = new Methods (ConstantPool, this, stream);
4848
Attributes = new AttributeCollection (ConstantPool, stream);
4949

src/Xamarin.Android.Tools.Bytecode/Fields.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public sealed class Fields : Collection<FieldInfo> {
1010

1111
public ConstantPool ConstantPool {get; private set;}
1212

13-
public Fields (ConstantPool constantPool, Stream stream)
13+
public Fields (ConstantPool constantPool, ClassFile declaringClass, Stream stream)
1414
{
1515
if (constantPool == null)
1616
throw new ArgumentNullException ("constantPool");
@@ -20,7 +20,7 @@ public Fields (ConstantPool constantPool, Stream stream)
2020
ConstantPool = constantPool;
2121
var count = stream.ReadNetworkUInt16 ();
2222
for (int i = 0; i < count; ++i) {
23-
Add (new FieldInfo (constantPool, stream));
23+
Add (new FieldInfo (constantPool, declaringClass, stream));
2424
}
2525
}
2626
}
@@ -31,13 +31,15 @@ public sealed class FieldInfo {
3131
ushort descriptorIndex;
3232

3333
public ConstantPool ConstantPool {get; private set;}
34-
public FieldAccessFlags AccessFlags {get; private set;}
34+
public ClassFile DeclaringType {get; private set;}
35+
public FieldAccessFlags AccessFlags {get; set;}
3536
public AttributeCollection Attributes {get; private set;}
3637
public string? KotlinType {get; set;}
3738

38-
public FieldInfo (ConstantPool constantPool, Stream stream)
39+
public FieldInfo (ConstantPool constantPool, ClassFile declaringType, Stream stream)
3940
{
4041
ConstantPool = constantPool;
42+
DeclaringType = declaringType;
4143
AccessFlags = (FieldAccessFlags) stream.ReadNetworkUInt16 ();
4244
nameIndex = stream.ReadNetworkUInt16 ();
4345
descriptorIndex = stream.ReadNetworkUInt16 ();
@@ -61,6 +63,8 @@ public string Descriptor {
6163
var signature = Attributes.Get<SignatureAttribute> ();
6264
return signature != null ? signature.Value : null;
6365
}
66+
67+
public bool IsPubliclyVisible => AccessFlags.HasFlag (FieldAccessFlags.Public) || AccessFlags.HasFlag (FieldAccessFlags.Protected);
6468
}
6569

6670
[Flags]
@@ -74,5 +78,8 @@ public enum FieldAccessFlags {
7478
Transient = 0x0080,
7579
Synthetic = 0x1000,
7680
Enum = 0x4000,
81+
82+
// This is not a real Java FieldAccessFlags, it is used to denote Kotlin "internal" access.
83+
Internal = 0x10000000,
7784
}
7885
}

src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ static MethodAccessFlags SetVisibility (MethodAccessFlags existing, MethodAccess
129129
return existing;
130130
}
131131

132+
static FieldAccessFlags SetVisibility (FieldAccessFlags existing, FieldAccessFlags newVisibility)
133+
{
134+
// First we need to remove any existing visibility flags,
135+
// without modifying other flags like Abstract
136+
existing = (existing ^ FieldAccessFlags.Public) & existing;
137+
existing = (existing ^ FieldAccessFlags.Protected) & existing;
138+
existing = (existing ^ FieldAccessFlags.Private) & existing;
139+
existing = (existing ^ FieldAccessFlags.Internal) & existing;
140+
141+
existing |= newVisibility;
142+
143+
return existing;
144+
}
145+
132146
static void FixupJavaMethods (Methods methods)
133147
{
134148
// We do the following method level fixups here because we can operate on all methods,
@@ -294,6 +308,15 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
294308
if (field is null)
295309
return;
296310

311+
// Hide field if it isn't Public/Protected
312+
if (!metadata.Flags.IsPubliclyVisible ()) {
313+
314+
if (field.IsPubliclyVisible) {
315+
Log.Debug ($"Kotlin: Hiding internal field {field.DeclaringType?.ThisClass.Name.Value} - {field.Name}");
316+
field.AccessFlags = SetVisibility (field.AccessFlags, FieldAccessFlags.Internal);
317+
}
318+
}
319+
297320
// Handle erasure of Kotlin unsigned types
298321
field.KotlinType = GetKotlinType (field.Descriptor, metadata.ReturnType?.ClassName);
299322
}

src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,8 @@ static string EscapeLiteral (string value)
593593

594594
static string GetVisibility (FieldAccessFlags accessFlags)
595595
{
596+
if (accessFlags.HasFlag (FieldAccessFlags.Internal))
597+
return "kotlin-internal";
596598
if ((accessFlags & FieldAccessFlags.Public) != 0)
597599
return "public";
598600
if ((accessFlags & FieldAccessFlags.Protected) != 0)

tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinFixupsTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,22 @@ public void HideInternalMethod ()
135135
Assert.True (output.Contains ("visibility=\"kotlin-internal\""));
136136
}
137137

138+
[Test]
139+
public void HideInternalField ()
140+
{
141+
var klass = LoadClassFile ("InternalField.class");
142+
var field = klass.Fields.First (m => m.Name == "city");
143+
144+
Assert.True (field.AccessFlags.HasFlag (FieldAccessFlags.Public));
145+
146+
KotlinFixups.Fixup (new [] { klass });
147+
148+
Assert.False (field.AccessFlags.HasFlag (FieldAccessFlags.Public));
149+
150+
var output = new XmlClassDeclarationBuilder (klass).ToXElement ().ToString ();
151+
Assert.True (output.Contains ("visibility=\"kotlin-internal\""));
152+
}
153+
138154
[Test]
139155
public void ParameterName ()
140156
{
Binary file not shown.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class InternalField {
2+
@JvmField
3+
internal val city: String = "London"
4+
}

0 commit comments

Comments
 (0)