Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

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

7 files changed

Lines changed: 57 additions & 5 deletions

File tree

‎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=newInterfaces(ConstantPool,stream);
46-
Fields=newFields(ConstantPool,stream);
46+
Fields=newFields(ConstantPool,this,stream);
4747
Methods=newMethods(ConstantPool,this,stream);
4848
Attributes=newAttributeCollection(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
publicConstantPoolConstantPool{get;privateset;}
1212

13-
publicFields(ConstantPoolconstantPool,Streamstream)
13+
publicFields(ConstantPoolconstantPool,ClassFiledeclaringClass,Streamstream)
1414
{
1515
if(constantPool==null)
1616
thrownewArgumentNullException("constantPool");
@@ -20,7 +20,7 @@ public Fields (ConstantPool constantPool, Stream stream)
2020
ConstantPool=constantPool;
2121
varcount=stream.ReadNetworkUInt16();
2222
for(inti=0;i<count;++i){
23-
Add(newFieldInfo(constantPool,stream));
23+
Add(newFieldInfo(constantPool,declaringClass,stream));
2424
}
2525
}
2626
}
@@ -31,13 +31,15 @@ public sealed class FieldInfo {
3131
ushortdescriptorIndex;
3232

3333
publicConstantPoolConstantPool{get;privateset;}
34-
publicFieldAccessFlagsAccessFlags{get;privateset;}
34+
publicClassFileDeclaringType{get;privateset;}
35+
publicFieldAccessFlagsAccessFlags{get;set;}
3536
publicAttributeCollectionAttributes{get;privateset;}
3637
publicstring?KotlinType{get;set;}
3738

38-
publicFieldInfo(ConstantPoolconstantPool,Streamstream)
39+
publicFieldInfo(ConstantPoolconstantPool,ClassFiledeclaringType,Streamstream)
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
varsignature=Attributes.Get<SignatureAttribute>();
6264
returnsignature!=null?signature.Value:null;
6365
}
66+
67+
publicboolIsPubliclyVisible=>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
returnexisting;
130130
}
131131

132+
staticFieldAccessFlagsSetVisibility(FieldAccessFlagsexisting,FieldAccessFlagsnewVisibility)
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+
returnexisting;
144+
}
145+
132146
staticvoidFixupJavaMethods(Methodsmethods)
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(fieldisnull)
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
staticstringGetVisibility(FieldAccessFlagsaccessFlags)
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+
publicvoidHideInternalField()
140+
{
141+
varklass=LoadClassFile("InternalField.class");
142+
varfield=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+
varoutput=newXmlClassDeclarationBuilder(klass).ToXElement().ToString();
151+
Assert.True(output.Contains("visibility=\"kotlin-internal\""));
152+
}
153+
138154
[Test]
139155
publicvoidParameterName()
140156
{
Binary file not shown.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
classInternalField {
2+
@JvmField
3+
internalval city:String="London"
4+
}

0 commit comments

Comments
 (0)