-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[class-parse, generator] Allow users to expose Kotlin internal types/members with metadata. #793
Conversation
Release note:
|
@@ -103,12 +103,12 @@ List<GenBase> ParsePackage (XElement ns, Predicate<XElement> p) | |||
GenBase gen = null; | |||
switch (elem.Name.LocalName) { | |||
case "class": | |||
if (elem.XGetAttribute ("obfuscated") == "true") | |||
if (elem.XGetAttribute ("obfuscated") == "true" || elem.XGetAttribute ("visibility") == "internal") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this change and the related one for "interface"
: why do we need to check for ./@visibility='internal'
here, but not visibility = "" (not set)/private
/etc.?
How is non-public
visibility handled elsewhere, and why is internal
different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
members are not emitted by class-parse
, so they should never exist in the api.xml
. (This is how we hide Kotlin internal types today: we set them to private
in class-parse
so they do not appear in the api.xml
.)
not-set
members are a little different. We still import them so they can participate in Java type resolution, and there's some logic about copying package-private
interface methods directly to types. However we do not emit them as managed types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not-set
members are a little different. … However we do not emit them as managed types.
How does that work? (The 'obvious-to-me' answer would be for e.g. XmlApiImporter.CreateClass()
to return null
when Visibility==""
, but that isn't the case -- there are no early returns in XmlApiImporter.CreateClass()
.)
Would it make more sense to treat Visibility=="internal"
the same way we treat Visibility==""
?
Not understanding how "not-set" visibility works is confusing to me, actually; java.util.concurrent.ConcurrentHashMap.CollectionView
has no visibility (useful example) and isn't overridden in metadata (useful example) and isn't bound -- all yay! -- but why isn't it bound? I've lost the plot, and can't readily see where/why this type isn't bound.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"not-set" visibility is handled by:
c946321
to
94c2bc5
Compare
Commit message for review: Summary:
Body: Fixes: https://github.com/xamarin/java.interop/issues/790
Context: https://github.com/xamarin/java.interop/issues/789
Commit 439bd839 bound Kotlin `internal` types as if they were Java
`private` types, which had the effect of *removing* them from
`api.xml` *before* `Metadata.xml` is processed, because `class-parse`
[doesn't write out `private` members][0]. Consequently, if you
wanted to mak an `internal` type *`public`* -- because that's what
*Java* sees, and thus may be needed for some interop scenarios --
you couldn't.
Update `class-parse` so that instead of emitting Kotlin `internal`
types and members as `private` members, they are instead emitted with
a `//@visibility` of `kotlin-internal`:
// Kotlin
internal class MyClass {
}
// api.xml
<class name="MyClass" visibility="kotlin-internal" … />
This causes the types to be "preserved" within `api.xml`.
Update `generator` so that types and members with a visibility of
`kotlin-internal` are *skipped* by default. These types shouldn't be
emitted as part of normal operation, as they're not *public* API.
Note: we introduce and use a new `//*/@visibility` value of
`kotline-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*.
[0]: https://github.com/xamarin/java.interop/blob/b46598a254c20060b107312564e0ec0aee9e33d6/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs#L32-L33 |
Alternative idea/question: should we instead update which is kinda/sorta equivalent to "reverting" https://github.com/xamarin/monodroid/commit/2c8110e6e7. @dellis1972 : do you remember why you wanted to skip |
I am warming up to the idea of putting |
I also prefer the idea of putting |
Research notes:
The existing behavior for private types seems like exactly what we want: they are discarded immediately after being imported. 👍 However the existing behavior for private members seems problematic. Ideally we would immediately discard these as well, however we do not. In some cases they generate uncompilable code ( It is conceivable that a user may be relying on the current
Update: Verified with Matthew that people do indeed do ^^:
This example provides public |
I don't think that there's a problem with relying on the linker to remove |
…methods with metadata.
Updated code and PR description to reflect the approach we settled on:
|
New & Improved™ commit message? Fixes: https://github.com/xamarin/java.interop/issues/790
Kotlin compiled for Java Bytecode is an interesting beast, as it has
features which aren't directly supported by Java bytecode.
One such feature is visibility: Kotlin supports an [`internal`][0]
visibility on types and members:
internal class Example
Java doesn't have a direct equivalent to `internal`; instead,
Java Bytecode uses a visibility of *`public`*:
% javap Example.class
public final class Example {
public Example();
}
Commit 439bd839 added support to `Xamarin.Android.Tools.Bytecode.dll`
for parsing Kotlin metadata. The result is that Kotlin `internal`
are marked as *`private`*, which causes them to be *skipped* and not
present within `api.xml`, because `class-parse`
[doesn't write out `private` members][1].
The result was that `Metadata.xml` could not be used to mark Kotlin
`internal` types as `public`, as they didn't exist within `api.xml`,
and thus couldn't serve as a "target" for `Metadata.xml`.
Improve support for using `Metadata.xml` to update Kotlin `internal`
types by making the following changes:
* `XmlClassDeclarationBuilder` now emits *all* types, even
`private` types. This includes Kotlin `internal` types which
were changed to have `private` visibility.
* Kotlin `internal` members are emitted into `api.xml` with a new
`//*/@visibility` value of `kotlin-internal`.
These changes allow the Kotlin declaration:
internal class Example {
public fun pubFun() {}
internal fun intFun() {}
}
to be emitted into `api.xml` a'la:
<class name="Example" visibility="private" …>
<method name="pubFun" visibility="public" …/>
<method name="intFun$main" visibility="kotlin-internal" …/>
</class>
[0]: https://kotlinlang.org/docs/visibility-modifiers.html#modules
[0]: https://github.com/xamarin/java.interop/blob/b46598a254c20060b107312564e0ec0aee9e33d6/src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs#L32-L33 |
Fixes: dotnet#1096 Context: https://stackoverflow.com/questions/57358750/module-info-class-file-is-different-in-the-module-jar-file-and-compiled-module-i Context: 678c4bd 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, the update `XmlClassDeclarationBuilder` so that it updates all `public` types *outside* of the "exported" packages to have a visibility of `kotlin-internal`. Why an `//*/@visibility` value of `kotlin-internal`? From a [suggestion][2] for the commit message of 678c4bd, which was sadly overlooked in the final draft: > Note: we introduce and use a new `//*/@visibility` value of > `kotline-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*. If we use `internal`, *those types are still bound*, it's just that the bound types have C# `internal` visibility, while we *want* them to be *skipped entirely*. A visibility value of `kotlin-internal` allows us to skip them, which is desired. `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" … visibility="internal" /> Note that `com.xamarin.internal.PublicClassNotInModuleExports` is now shown as `internal` instead of `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]: dotnet#793 (comment)
Fixes: dotnet#1096 Context: https://stackoverflow.com/questions/57358750/module-info-class-file-is-different-in-the-module-jar-file-and-compiled-module-i Context: 678c4bd 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, the update `XmlClassDeclarationBuilder` so that it updates all `public` types *outside* of the "exported" packages to have a visibility of `kotlin-internal`. Why a `//*/@visibility` value of `kotlin-internal`? From a [suggestion][2] for the commit message of 678c4bd, which was sadly overlooked in the final merge: > 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*. If we use `internal`, *those types are still bound*, it's just that the bound types have C# `internal` visibility, while we *want* them to be *skipped entirely*. A visibility value of `kotlin-internal` allows us to skip them, which is desired. `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" … visibility="kotlin-internal" /> Note that `com.xamarin.internal.PublicClassNotInModuleExports` is now shown as `kotlin-internal` instead of `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]: dotnet#793 (comment)
Fixes: dotnet#1096 Context: https://stackoverflow.com/questions/57358750/module-info-class-file-is-different-in-the-module-jar-file-and-compiled-module-i Context: 678c4bd 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, the update `XmlClassDeclarationBuilder` so that it updates all `public` types *outside* of the "exported" packages to have a visibility of `kotlin-internal`. Why a `//*/@visibility` value of `kotlin-internal`? From a [suggestion][2] for the commit message of 678c4bd, which was sadly overlooked in the final merge: > 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*. If we use `internal`, *those types are still bound*, it's just that the bound types have C# `internal` visibility, while we *want* them to be *skipped entirely*. A visibility value of `kotlin-internal` allows us to skip them, which is desired. `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" … visibility="kotlin-internal" /> Note that `com.xamarin.internal.PublicClassNotInModuleExports` is now shown as `kotlin-internal` instead of `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]: dotnet#793 (comment)
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)
Fixes #790.
Modify
class-parse
to emit Kotlininternal
types/members so that users can usemetadata
to change them topublic
if they wish, giving them the same access they would have if consumed from Java. Ifvisibility
is not changed topublic
by the user,generator
will ignore importing these types/members, resulting in the same bindings as today.Format is the same as existing metadata visibility fixup:
class-parse
now emits allprivate
types asvisibility="private"
because the existinggenerator
behavior will ignore anyprivate
types, which is what we want. Kotlininternal
types were already being changed toprivate
, so they fall into this category.class-parse
now emits Kotlininternal
members asvisibility="kotlin-internal"
because the existinggenerator
behavior is that members marked asinternal
orprivate
are actually bound asinternal/private
members. Whether this was ever intentional is debatable, but it is a behavior that user code in the wild relies on that we do not wish to break.There are a couple of issues with simply allowing members to be bound as
internal/private
:generator
bug where attempting to bind aprivate
member results in invalid C# code. This casts doubt that this was ever "supported", and thus there could be an unknown number of other bugs users would begin hitting if we started emitting a bunch ofprivate
members.