Background
Kotlin @JvmInline value class parameters strip the constructor of a
binding even when the JVM bytecode signature is plain primitive shape and
the surrounding type is otherwise fully bound. This is a constructor-side
sibling of dotnet/java-interop#1440 (which targets static methods like
Button-bWB7cM8 / Typography(...)) and the closest analogue is
#1453 — but
that issue focuses on a stripped static factory function. Stripped
constructors are not currently tracked.
Concrete repro: androidx.compose.ui.graphics.SolidColor
Kotlin source (1.11.x):
@Immutable
class SolidColor(val value: Color) : ShaderBrush() { … }
Color is @JvmInline value class Color(val value: ULong), so the JVM
bytecode for the ctor is plain primitive — <init>(J)V. There is no
mangling on the constructor itself (Kotlin's hash-mangling only applies
to methods), but the binder still drops it.
Xamarin.AndroidX.Compose.UI.Graphics.Android 1.11.2.1 decompiled with
ilspycmd:
[Register("androidx/compose/ui/graphics/SolidColor", DoNotGenerateAcw = true)]
public sealed class SolidColor : Brush, IInterpolatable, …
{
// value-class-param methods with mangled names ARE kept:
[Register("getValue-0d7_KjU", "()J", "")] public long Value { get; }
[Register("applyTo-Pq9zytI", "(JLandroidx/compose/ui/graphics/Paint;F)V", "")]
public sealed override void ApplyTo(long size, IPaint p, float alpha) { … }
[Register("lerp", "(Ljava/lang/Object;F)Ljava/lang/Object;", "")]
public Object? Lerp(Object? other, float t) { … }
// Only the internal acw ctor survives. The public (J)V ctor is gone:
internal SolidColor(nint javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer) { }
// <-- where is `public SolidColor(long packedColor)` ?
}
source/androidx.compose.ui/ui-graphics-android/PublicAPI/PublicAPI.Unshipped.txt
confirms the gap — the only SolidColor ctor in the public API is the
internal (nint, JniHandleOwnership) one. There is no SolidColor(long).
This is not unique to SolidColor: every Compose class whose primary
ctor takes a value-class parameter has the same problem (Path,
Outline.Rectangle, several BrushPainter shapes, …). The
androidx.compose.ui.graphics.Color type itself loses both its (J)V
ctor and its box-impl(J)Color static factory for the same reason.
Real-world impact
jonathanpeppers/Microsoft.AndroidX.Compose#251 adds
Brush.SolidColor(Color) and the gradient factories. Because the bound
SolidColor has no callable ctor, the PR ships a hand-written JNI
fallback in BrushBridges.cs:
// `new androidx.compose.ui.graphics.SolidColor(Color)` — the ctor
// is stripped because its parameter is a value-class `Color`.
internal static unsafe IntPtr BrushSolidColor(long color)
{
if (s_solidColor_ctor == IntPtr.Zero)
{
s_solidColor_class = Java.Lang.Class.FromType(
typeof(AndroidX.Compose.UI.Graphics.SolidColor)).Handle;
s_solidColor_ctor = JNIEnv.GetMethodID(
s_solidColor_class, "<init>", "(J)V");
}
var args = stackalloc JValue[1];
args[0] = new JValue(color);
return JNIEnv.NewObject(s_solidColor_class, s_solidColor_ctor, args);
}
The same PR also reaches for Color.box-impl(J) (a static factory) and
Brush.Companion (a static field) directly via JNI — those have other
trackers — but the ctor case is the new one and is what this issue is
about.
Question: would <add-node> + <constructor> be an acceptable metadata workaround until #1440 lands?
There is precedent in this repo for synthesizing missing
constructors via <add-node>:
Applied to SolidColor, the proposed metadata would look like:
<add-node path="/api/package[@name='androidx.compose.ui.graphics']/class[@name='SolidColor']">
<constructor
deprecated="not deprecated" final="false"
name="SolidColor" static="false" visibility="public"
bridge="false" synthetic="false"
jni-signature="(J)V">
<parameter name="value" type="long" jni-type="J" />
</constructor>
</add-node>
The bytecode-level ctor <init>(J)V already exists — the binder just
isn't emitting it because Kotlin metadata flags the param as a
value class. Manually re-introducing it via <add-node> should cause
the generator to bind it as a normal public SolidColor(long value),
the same way the existing applyTo-Pq9zytI(JL…/Paint;F)V and
getValue-0d7_KjU()J methods are bound today (those keep their
hash-mangled JNI names and project the value-class params as their
underlying primitives).
Question for maintainers: is the <add-node> + <constructor> recipe
above an accepted workaround pattern for value-class-param ctors? If so,
it would unblock several hand-rolled-JNI hotspots in
Microsoft.AndroidX.Compose (SolidColor, Color's box-impl,
several Painter shapes) ahead of the eventual #1440 generator fix.
If <add-node> for value-class-param ctors is not expected to work
(e.g. there's a downstream fixup that resurfaces the strip), please say
so here and we'll keep the JNI bridges. Either answer is useful.
Why a separate issue from the existing trackers
| Issue |
Scope |
Mechanism |
dotnet/java-interop#1431 (open) |
Umbrella: inline-class projection + sibling-collision |
Generator |
dotnet/java-interop#1432 (merged) |
Phase 1 — sibling-collision dedup for hashed methods |
Generator |
dotnet/java-interop#1440 (open) |
Phase 2 — project inline classes as readonly structs |
Generator |
dotnet/android-libraries#1453 (open) |
Stripped TypographyKt.Typography(...) static factory |
Material3 binding + (#1440) |
| This issue |
Stripped constructors of types whose Kotlin primary ctor takes a value class |
UI.Graphics binding + Metadata.xml |
#1432/#1440/#1431 cover the method side (mangled names like
Button-bWB7cM8). Constructors aren't mangled (they're always <init>),
so the issue presents differently — but the same Kotlin-metadata-driven
strip path drops them. This issue exists so that both halves of the
pattern are tracked, and so the metadata workaround question can be
answered explicitly.
References
Background
Kotlin
@JvmInline value classparameters strip the constructor of abinding even when the JVM bytecode signature is plain primitive shape and
the surrounding type is otherwise fully bound. This is a constructor-side
sibling of
dotnet/java-interop#1440(which targets static methods likeButton-bWB7cM8/Typography(...)) and the closest analogue is#1453 — but
that issue focuses on a stripped static factory function. Stripped
constructors are not currently tracked.
Concrete repro:
androidx.compose.ui.graphics.SolidColorKotlin source (1.11.x):
Coloris@JvmInline value class Color(val value: ULong), so the JVMbytecode for the ctor is plain primitive —
<init>(J)V. There is nomangling on the constructor itself (Kotlin's hash-mangling only applies
to methods), but the binder still drops it.
Xamarin.AndroidX.Compose.UI.Graphics.Android1.11.2.1 decompiled withilspycmd:source/androidx.compose.ui/ui-graphics-android/PublicAPI/PublicAPI.Unshipped.txtconfirms the gap — the only
SolidColorctor in the public API is theinternal
(nint, JniHandleOwnership)one. There is noSolidColor(long).This is not unique to
SolidColor: every Compose class whose primaryctor takes a value-class parameter has the same problem (
Path,Outline.Rectangle, severalBrushPaintershapes, …). Theandroidx.compose.ui.graphics.Colortype itself loses both its(J)Vctor and its
box-impl(J)Colorstatic factory for the same reason.Real-world impact
jonathanpeppers/Microsoft.AndroidX.Compose#251addsBrush.SolidColor(Color)and the gradient factories. Because the boundSolidColorhas no callable ctor, the PR ships a hand-written JNIfallback in
BrushBridges.cs:The same PR also reaches for
Color.box-impl(J)(a static factory) andBrush.Companion(a static field) directly via JNI — those have othertrackers — but the ctor case is the new one and is what this issue is
about.
Question: would
<add-node>+<constructor>be an acceptable metadata workaround until #1440 lands?There is precedent in this repo for synthesizing missing
constructors via
<add-node>:source/com.github.bumptech.glide/glide/Transforms/Metadata.xml#L100-L101injects a public ctor into
GeneratedAppGlideModule, and theresulting
Bumptech.Glide.GeneratedAppGlideModule.GeneratedAppGlideModule() -> voidshows up in
PublicAPI.Unshipped.txt.source/com.google.android.gms/play-services-iid/Transforms/Metadata.xml#L26-L28injects an
<init>()Vctor onInstanceIDListenerServicewith anexplicit
jni-signature="()V", and that surfaces asAndroid.Gms.Iid.InstanceIDListenerService.InstanceIDListenerService() -> void.Applied to
SolidColor, the proposed metadata would look like:The bytecode-level ctor
<init>(J)Valready exists — the binder justisn't emitting it because Kotlin metadata flags the param as a
value class. Manually re-introducing it via
<add-node>should causethe generator to bind it as a normal
public SolidColor(long value),the same way the existing
applyTo-Pq9zytI(JL…/Paint;F)VandgetValue-0d7_KjU()Jmethods are bound today (those keep theirhash-mangled JNI names and project the value-class params as their
underlying primitives).
Question for maintainers: is the
<add-node>+<constructor>recipeabove an accepted workaround pattern for value-class-param ctors? If so,
it would unblock several hand-rolled-JNI hotspots in
Microsoft.AndroidX.Compose(SolidColor,Color'sbox-impl,several
Paintershapes) ahead of the eventual #1440 generator fix.If
<add-node>for value-class-param ctors is not expected to work(e.g. there's a downstream fixup that resurfaces the strip), please say
so here and we'll keep the JNI bridges. Either answer is useful.
Why a separate issue from the existing trackers
dotnet/java-interop#1431(open)dotnet/java-interop#1432(merged)dotnet/java-interop#1440(open)readonly structsdotnet/android-libraries#1453(open)TypographyKt.Typography(...)static factory#1432/#1440/#1431cover the method side (mangled names likeButton-bWB7cM8). Constructors aren't mangled (they're always<init>),so the issue presents differently — but the same Kotlin-metadata-driven
strip path drops them. This issue exists so that both halves of the
pattern are tracked, and so the metadata workaround question can be
answered explicitly.
References
jonathanpeppers/Microsoft.AndroidX.Compose#251— real-world consumerBrushBridges.cs— the JNI fallback this issue is asking to removeSolidColor.kt— upstream Kotlin source