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

Commit 07d5595

Browse files
authored
[class-parse] support Module AttributeInfo (#1097)
Fixes: #1096 Context: https://stackoverflow.com/questions/57358750/module-info-class-file-is-different-in-the-module-jar-file-and-compiled-module-i Context: 678c4bd Context: b274a67 JDK 9 adds support for [modules][0], which are (kinda sorta) like .NET Assemblies: modules can depend upon other modules, export packages, etc. In particular: > **exports and exports…to.** An exports module directive specifies > one of the module’s packages whose `public` types (and their nested > `public` and `protected` types) should be accessible to code in all > other modules. This allows an equivalent to the [C# `internal` access modifier][1]: `public` types in a *non-`export`ed package* should be treated as "internal", while `public` types in an `export`ed package are "fully public". Update `Xamarin.Android.Tools.Bytecode.dll` to extract the module- related information, then update `ClassPath` so that it updates all `public` types *outside* of the "exported" packages to have an `//*/@annotated-visibility` attribute value of `module-info`. (See also commit b274a67, which added `//*/@@annotated-visibility`.) If there is *already* an `//*/@annotated-visibility` value, then we *append* ` module-info` to the attribute value. We use `//*/@annotated-visibility` because we are concerned about introducing an ABI break into AndroidX-related bindings because of type visibility changes. If this isn't a concern, it should be possible to use Metadata to remove those types: <attr path="//class[@annotated-visibility]" name="visibility">kotlin-internal</attr> <attr path="//interface[@annotated-visibility]" name="visibility">kotlin-internal</attr> `class-parse` command-line parsing has been altered. There is now a "global `ClassPath`", which will be used to hold `.class` files provided on the command-line. `.jar` and `.jmod` files provided on the command-line will be given their own `ClassPath` instances, and `module-info.class`-based annotated-visibility fixups are specific to each `ClassPath` instance. Global files are processed together. There is thus no way for `module-info.class` visibility changes from `a.jar` to impact `b.jar`. After visibilities are fixed up, we then merge everything into the "global" `ClassPath` instance before transforming to XML. Additionally, `class-parse --dump` can now accept `.jar` files, and will dump out *all* `.class` filers within the `.jar` file. To make this output easier, each "entry" starts with a "header" of `-- Begin {ClassFile.FullJniName}`, and a blank link will be printed between each entry. `tests/Xamarin.Android.Tools.Bytecode-Tests` has been updated to: 1. Contain a `module-info.java`, which declares a `com.xamarin` module. 2. Add a new `com.xamarin.internal.PublicClassNotInModuleExports` type which is *not* in the `com.xamarin` package, but instead a *nested* package. The type is `public`. 3. Build a `xatb.jar` artifact This makes for a simple one-off test: % dotnet build tests/Xamarin.Android.Tools.Bytecode-Tests/*.csproj % dotnet build tools/class-parse/*.csproj % dotnet bin/Debug-net7.0/class-parse.dll \ tests/Xamarin.Android.Tools.Bytecode-Tests/obj/Debug-net7.0/xatb.jar … <class name="PublicClassNotInModuleExports" … annotated-visibility="module-info" /> Note that `com.xamarin.internal.PublicClassNotInModuleExports` now has an XML attribute `annotated-visibility="module-info"`. Aside: the commit message of 678c4bd sadly overlooked this [clarification][2] for why `kotlin-internal` was introduced: > Note: we introduce and use a new `//*/@visibility` value of > `kotlin-internal` because `internal` is an *existing* value that may > be used in `Metadata.xml` files, e.g. making `public` API `internal` > so that it can still be used in the binding, but isn't *public*. Aside: a discovered oddity: `jar cf …` *modifies* `module-info.class`, adding a `ModulePackages` attribute! (Specifically, if you compare the "on-disk" `module-info.class` to the one within `tests/Xamarin.Android.Tools.Bytecode-Tests/obj/$(Configuration)/xatb.jar`, they differ in size!) [0]: https://www.oracle.com/corporate/features/understanding-java-9-modules.html [1]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/internal [2]: #793 (comment)
1 parent f0e3300 commit 07d5595

14 files changed

Lines changed: 475 additions & 46 deletions

‎src/Xamarin.Android.Tools.Bytecode/AttributeInfo.cs‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class AttributeInfo {
4444
publicconststringInnerClasses="InnerClasses";
4545
publicconststringLocalVariableTable="LocalVariableTable";
4646
publicconststringMethodParameters="MethodParameters";
47+
publicconststringModule="Module";
48+
publicconststringModulePackages="ModulePackages";
4749
publicconststringSignature="Signature";
4850
publicconststringSourceFile="SourceFile";
4951
publicconststringStackMapTable="StackMapTable";
@@ -79,6 +81,8 @@ public string Name {
7981
{typeof(InnerClassesAttribute),InnerClasses},
8082
{typeof(LocalVariableTableAttribute),LocalVariableTable},
8183
{typeof(MethodParametersAttribute),MethodParameters},
84+
{typeof(ModuleAttribute),Module},
85+
{typeof(ModulePackagesAttribute),ModulePackages},
8286
{typeof(RuntimeVisibleAnnotationsAttribute),RuntimeVisibleAnnotations},
8387
{typeof(RuntimeInvisibleAnnotationsAttribute),RuntimeInvisibleAnnotations},
8488
{typeof(SignatureAttribute),Signature},
@@ -98,6 +102,7 @@ internal static string GetAttributeName<T>()
98102
publicstaticAttributeInfoCreateFromStream(ConstantPoolconstantPool,Streamstream)
99103
{
100104
varnameIndex=stream.ReadNetworkUInt16();
105+
varconstant=constantPool[nameIndex];
101106
varname=((ConstantPoolUtf8Item)constantPool[nameIndex]).Value;
102107
varattr=CreateAttribute(name,constantPool,nameIndex,stream);
103108
returnattr;
@@ -114,6 +119,8 @@ static AttributeInfo CreateAttribute (string name, ConstantPool constantPool, us
114119
caseInnerClasses:returnnewInnerClassesAttribute(constantPool,nameIndex,stream);
115120
caseLocalVariableTable:returnnewLocalVariableTableAttribute(constantPool,nameIndex,stream);
116121
caseMethodParameters:returnnewMethodParametersAttribute(constantPool,nameIndex,stream);
122+
caseModule:returnnewModuleAttribute(constantPool,nameIndex,stream);
123+
caseModulePackages:returnnewModulePackagesAttribute(constantPool,nameIndex,stream);
117124
caseRuntimeVisibleAnnotations:returnnewRuntimeVisibleAnnotationsAttribute(constantPool,nameIndex,stream);
118125
caseRuntimeInvisibleAnnotations:returnnewRuntimeInvisibleAnnotationsAttribute(constantPool,nameIndex,stream);
119126
caseRuntimeInvisibleParameterAnnotations:returnnewRuntimeInvisibleParameterAnnotationsAttribute(constantPool,nameIndex,stream);
@@ -503,6 +510,118 @@ public override string ToString ()
503510
}
504511
}
505512

513+
// https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.25
514+
publicsealedclassModuleAttribute:AttributeInfo
515+
{
516+
ushortmoduleNameIndex;
517+
publicstringModuleName{
518+
get{return((ConstantPoolModuleItem)ConstantPool[moduleNameIndex]).Name.Value;}
519+
}
520+
521+
publicModuleFlagsModuleFlags{get;privateset;}
522+
523+
ushortmoduleVersionIndex;
524+
publicstring?ModuleVersion{
525+
get{return((ConstantPoolUtf8Item)ConstantPool[moduleVersionIndex])?.Value;}
526+
}
527+
528+
publicCollection<ModuleRequiresInfo>Requires{get;}=new();
529+
publicCollection<ModuleExportsPackageInfo>Exports{get;}=new();
530+
publicCollection<ModuleOpensPackageInfo>Opens{get;}=new();
531+
publicCollection<ConstantPoolClassItem>Uses{get;}=new();
532+
publicCollection<ModuleProvidesInfo>Provides{get;}=new();
533+
534+
publicModuleAttribute(ConstantPoolconstantPool,ushortnameIndex,Streamstream)
535+
:base(constantPool,nameIndex,stream)
536+
{
537+
varattribute_length=stream.ReadNetworkUInt32();
538+
539+
moduleNameIndex=stream.ReadNetworkUInt16();
540+
ModuleFlags=(ModuleFlags)stream.ReadNetworkUInt16();
541+
moduleVersionIndex=stream.ReadNetworkUInt16();
542+
543+
varrequires_count=stream.ReadNetworkUInt16();
544+
for(inti=0;i<requires_count;++i){
545+
Requires.Add(newModuleRequiresInfo(constantPool,stream));
546+
}
547+
548+
varexports_count=stream.ReadNetworkUInt16();
549+
for(inti=0;i<exports_count;++i){
550+
Exports.Add(newModuleExportsPackageInfo(constantPool,stream));
551+
}
552+
553+
varopens_count=stream.ReadNetworkUInt16();
554+
for(inti=0;i<opens_count;++i){
555+
Opens.Add(newModuleOpensPackageInfo(constantPool,stream));
556+
}
557+
558+
varuses_count=stream.ReadNetworkUInt16();
559+
for(inti=0;i<uses_count;++i){
560+
varuses_index=stream.ReadNetworkUInt16();
561+
Uses.Add((ConstantPoolClassItem)constantPool[uses_index]);
562+
}
563+
564+
varprovides_count=stream.ReadNetworkUInt16();
565+
for(inti=0;i<provides_count;++i){
566+
Provides.Add(newModuleProvidesInfo(constantPool,stream));
567+
}
568+
}
569+
570+
publicoverridestringToString()
571+
{
572+
vars=newStringBuilder()
573+
.Append("Module(").AppendLine()
574+
.Append(" ").Append(nameof(ModuleName)).Append("='").Append(ModuleName).AppendLine("', ")
575+
.Append(" ").Append(nameof(ModuleVersion)).Append("='").Append(ModuleVersion).Append("'");
576+
AppendString(s,nameof(Requires),Requires);
577+
AppendString(s,nameof(Exports),Exports);
578+
AppendString(s,nameof(Opens),Opens);
579+
AppendString(s,nameof(Uses),Uses.Select(u =>$"UsesService({u.Name})").ToList());
580+
AppendString(s,nameof(Provides),Provides);
581+
s.Append(")");
582+
583+
returns.ToString();
584+
}
585+
586+
staticStringBuilderAppendString<T>(StringBuilders,stringcollectionName,IList<T>items)
587+
{
588+
if(items.Count==0){
589+
returns;
590+
}
591+
s.AppendLine(",");
592+
s.Append(" ").Append(collectionName).AppendLine("={");
593+
s.Append(" ").Append(items[0]);
594+
for(inti=1;i<items.Count;++i){
595+
s.AppendLine(",");
596+
s.Append(" ");
597+
s.Append(items[i]);
598+
}
599+
returns.Append("}");
600+
}
601+
}
602+
603+
// https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.26
604+
publicsealedclassModulePackagesAttribute:AttributeInfo{
605+
publicCollection<ConstantPoolPackageItem>Packages{get;}=new();
606+
607+
publicModulePackagesAttribute(ConstantPoolconstantPool,ushortnameIndex,Streamstream)
608+
:base(constantPool,nameIndex,stream)
609+
{
610+
varattribute_length=stream.ReadNetworkUInt32();
611+
612+
varpackage_count=stream.ReadNetworkUInt16();
613+
for(inti=0;i<package_count;++i){
614+
varpackage_index=stream.ReadNetworkUInt16();
615+
Packages.Add((ConstantPoolPackageItem)constantPool[package_index]);
616+
}
617+
}
618+
619+
publicoverridestringToString()
620+
{
621+
return$"ModulePackages({{{string.Join (",", Packages.Select (p => p.Name.Value))}}})";
622+
}
623+
}
624+
506625
// https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.16
507626
publicsealedclassRuntimeVisibleAnnotationsAttribute:AttributeInfo
508627
{

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,15 @@ public ClassFile (Stream stream)
4848
Attributes=newAttributeCollection(ConstantPool,stream);
4949

5050
inte=stream.ReadByte();
51-
if(e>=0)
52-
thrownewBadImageFormatException("Stream has trailing data?!");
51+
if(e>=0){
52+
vartrailing=newSystem.Text.StringBuilder();
53+
trailing.AppendFormat("{0:x2}",(byte)e);
54+
while((e=stream.ReadByte())>=0){
55+
trailing.Append(" ");
56+
trailing.AppendFormat("{0:x2}",(byte)e);
57+
}
58+
thrownewBadImageFormatException($"Stream has trailing data?! {{{trailing}}}");
59+
}
5360
}
5461

5562
publicstaticboolIsClassFile(Streamstream)
@@ -213,6 +220,10 @@ public enum ClassAccessFlags {
213220
Synthetic=0x1000,
214221
Annotation=0x2000,
215222
Enum=0x4000,
223+
Module=0x8000,
224+
225+
// This is not a real Java ClassAccessFlags, it is used to denote non-exported public types
226+
Internal=0x10000000,
216227
}
217228
}
218229

‎src/Xamarin.Android.Tools.Bytecode/ClassPath.cs‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,6 @@ static bool ShouldLoadEntry (ZipArchiveEntry entry)
8888
if(entry.Length==0)
8989
returnfalse;
9090

91-
if(entry.Name=="module-info.class")
92-
returnfalse;
93-
9491
if(entry.Name.EndsWith(".jnilib",StringComparison.OrdinalIgnoreCase))
9592
returnfalse;
9693

@@ -111,6 +108,14 @@ public void Add (ClassFile classFile)
111108
classFiles.Add(classFile);
112109
}
113110

111+
publicvoidAdd(ClassPathclassPath,boolremoveModules=true)
112+
{
113+
classPath.FixupModuleVisibility(removeModules);
114+
foreach(varcinclassPath.classFiles){
115+
Add(c);
116+
}
117+
}
118+
114119
publicReadOnlyDictionary<string,List<ClassFile>>GetPackages()
115120
{
116121
returnnewReadOnlyDictionary<string,List<ClassFile>>(classFiles
@@ -360,6 +365,7 @@ public XElement ToXElement ()
360365
FixUpParametersFromClasses();
361366

362367
KotlinFixups.Fixup(classFiles);
368+
FixupModuleVisibility(removeModules:true);
363369

364370
varpackagesDictionary=GetPackages();
365371
varapi=newXElement("api",
@@ -375,6 +381,42 @@ packagesDictionary [p].OrderBy (c => c.ThisClass.Name.Value, StringComparer.Ordi
375381
returnapi;
376382
}
377383

384+
publicvoidFixupModuleVisibility(boolremoveModules)
385+
{
386+
varpublicPackages=newHashSet<string>();
387+
388+
varmoduleFiles=classFiles.Where(c =>c.AccessFlags==ClassAccessFlags.Module)
389+
.ToList();
390+
if(moduleFiles.Count==0){
391+
return;
392+
}
393+
foreach(varmoduleFileinmoduleFiles){
394+
if(removeModules){
395+
classFiles.Remove(moduleFile);
396+
}
397+
foreach(varmoduleAttrinmoduleFile.Attributes.OfType<ModuleAttribute>()){
398+
foreach(varexportinmoduleAttr.Exports){
399+
publicPackages.Add(export.Exports);
400+
}
401+
}
402+
}
403+
404+
foreach(varcinclassFiles){
405+
if(!c.AccessFlags.HasFlag(ClassAccessFlags.Public)){
406+
continue;
407+
}
408+
varjniName=c.ThisClass.Name.Value;
409+
varpackageEnd=jniName.LastIndexOf('/');
410+
if(packageEnd<0){
411+
continue;
412+
}
413+
varpackage=jniName.Substring(0,packageEnd);
414+
if(!publicPackages.Contains(package)){
415+
c.AccessFlags=KotlinFixups.SetVisibility(c.AccessFlags,ClassAccessFlags.Internal);
416+
}
417+
}
418+
}
419+
378420
publicvoidSaveXmlDescription(stringfileName)
379421
{
380422
varencoding=newUTF8Encoding(encoderShouldEmitUTF8Identifier:false);
@@ -395,5 +437,7 @@ public void SaveXmlDescription (TextWriter textWriter)
395437
contents.Save(writer);
396438
textWriter.WriteLine();
397439
}
440+
441+
publicIEnumerable<ClassFile>GetClassFiles()=>classFiles;
398442
}
399443
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ static void FixupClassVisibility (ClassFile klass, KotlinClass metadata)
102102
}
103103

104104
// Passing null for 'newVisibility' parameter means 'package-private'
105-
staticClassAccessFlagsSetVisibility(ClassAccessFlagsexisting,ClassAccessFlags?newVisibility)
105+
internalstaticClassAccessFlagsSetVisibility(ClassAccessFlagsexisting,ClassAccessFlags?newVisibility)
106106
{
107107
// First we need to remove any existing visibility flags,
108108
// without modifying other flags like Abstract

0 commit comments

Comments
 (0)