From 125baaee84c99cada0b76b0324c0172f019232f0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 24 Jul 2026 14:22:36 -0400 Subject: [PATCH 1/2] Cross scalar props typed instead of as JSON literals --- .../kotlin/com/pureswift/swiftui/ViewNode.kt | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt b/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt index 945b808..d396c88 100644 --- a/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt +++ b/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt @@ -43,31 +43,68 @@ data class ViewNode( /// Bridge constructor: the Swift materializer builds nodes through this, /// crossing JNI with flat arrays (one call per node, arrays as single - /// arguments). Prop and modifier-arg values arrive as JSON literals - /// ("\"text\"", "42", "true"), keeping the typed JsonObject model without - /// per-field JNI calls. Negative count/provider mean "absent". + /// arguments). Scalar values cross typed — a kind tag selects between the + /// string slot and the bits slot (doubles bit-cast into the long, so both + /// 64-bit callback ids and doubles cross exactly) — so no value takes the + /// JSON parser except array-valued props. Modifier args flatten into one + /// run of slots, `modifierArgCounts` giving each modifier's share. + /// Negative count/provider mean "absent". constructor( type: String, id: String, propKeys: Array, - propValues: Array, + propKinds: IntArray, + propStrings: Array, + propBits: LongArray, modifierKinds: Array, - modifierArgs: Array, + modifierArgCounts: IntArray, + argKeys: Array, + argKinds: IntArray, + argStrings: Array, + argBits: LongArray, children: Array, count: Int, itemProviderId: Long, ) : this( type = type, id = id, - props = JsonObject(propKeys.indices.associate { propKeys[it] to Json.parseToJsonElement(propValues[it]) }), - modifiers = modifierKinds.indices.map { - ModifierNode(modifierKinds[it], Json.parseToJsonElement(modifierArgs[it]).jsonObject) + props = JsonObject(propKeys.indices.associate { + propKeys[it] to jsonValue(propKinds[it], propStrings[it], propBits[it]) + }), + modifiers = buildList { + var base = 0 + for (m in modifierKinds.indices) { + val argCount = modifierArgCounts[m] + add(ModifierNode(modifierKinds[m], JsonObject((0 until argCount).associate { + val i = base + it + argKeys[i] to jsonValue(argKinds[i], argStrings[i], argBits[i]) + }))) + base += argCount + } }, children = children.toList(), count = if (count >= 0) count else null, itemProviderId = if (itemProviderId >= 0) itemProviderId else null, ) + companion object { + // value kinds used by the bridge constructor + private const val KIND_STRING = 0 + private const val KIND_DOUBLE = 1 + private const val KIND_BOOL = 2 + private const val KIND_INT = 3 + private const val KIND_JSON = 4 + + private fun jsonValue(kind: Int, string: String, bits: Long): kotlinx.serialization.json.JsonElement = + when (kind) { + KIND_STRING -> JsonPrimitive(string) + KIND_DOUBLE -> JsonPrimitive(Double.fromBits(bits)) + KIND_BOOL -> JsonPrimitive(bits != 0L) + KIND_INT -> JsonPrimitive(bits) + else -> Json.parseToJsonElement(string) // arrays only + } + } + // Typed prop accessors used by the interpreter. fun string(key: String): String? = (props[key] as? JsonPrimitive)?.content From ddb4348657a40510056d1443443dace77e41291d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 24 Jul 2026 14:22:36 -0400 Subject: [PATCH 2/2] Materialize scalar props into typed slots --- Sources/ComposeUI/KotlinBindings.swift | 16 +++++-- Sources/ComposeUI/Materializer.swift | 59 +++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Sources/ComposeUI/KotlinBindings.swift b/Sources/ComposeUI/KotlinBindings.swift index ac6deca..5021723 100644 --- a/Sources/ComposeUI/KotlinBindings.swift +++ b/Sources/ComposeUI/KotlinBindings.swift @@ -14,16 +14,24 @@ import SwiftJava open class ViewNodeObject: JavaObject { /// The bridge constructor: one JNI call per node, arrays crossing as - /// single arguments. Prop/modifier-arg values are JSON literals; negative - /// count/provider mean "absent". + /// single arguments. Scalars cross typed — a kind tag per value selects + /// the string slot or the bits slot (doubles bit-cast into the long) — + /// so only array-valued props take the JSON path. Negative count/provider + /// mean "absent". @JavaMethod @_nonoverride public convenience init( _ type: String, _ id: String, _ propKeys: [String], - _ propValues: [String], + _ propKinds: [Int32], + _ propStrings: [String], + _ propBits: [Int64], _ modifierKinds: [String], - _ modifierArgs: [String], + _ modifierArgCounts: [Int32], + _ argKeys: [String], + _ argKinds: [Int32], + _ argStrings: [String], + _ argBits: [Int64], _ children: [ViewNodeObject?], _ count: Int32, _ itemProviderId: Int64, diff --git a/Sources/ComposeUI/Materializer.swift b/Sources/ComposeUI/Materializer.swift index 5e370ce..b94d2b2 100644 --- a/Sources/ComposeUI/Materializer.swift +++ b/Sources/ComposeUI/Materializer.swift @@ -11,28 +11,75 @@ import SwiftJava public enum Materializer { + // value kinds understood by the Kotlin bridge constructor + private static let kindString: Int32 = 0 + private static let kindDouble: Int32 = 1 + private static let kindBool: Int32 = 2 + private static let kindInt: Int32 = 3 + private static let kindJSON: Int32 = 4 + + /// One scalar's typed slots: strings ride the string slot; numbers and + /// bools ride the bits slot (doubles bit-cast, so 64-bit callback ids and + /// doubles both cross exactly); only arrays fall back to JSON text. + private static func slots(for value: PropValue) -> (kind: Int32, string: String, bits: Int64) { + switch value { + case .string(let string): + return (kindString, string, 0) + case .double(let double): + return (kindDouble, "", Int64(bitPattern: double.bitPattern)) + case .bool(let bool): + return (kindBool, "", bool ? 1 : 0) + case .int(let int): + return (kindInt, "", Int64(int)) + case .array: + return (kindJSON, jsonLiteral(value), 0) + } + } + /// Builds the Kotlin mirror of an IR tree, depth-first. public static func materialize(_ node: RenderNode) -> ViewNodeObject { var propKeys: [String] = [] - var propValues: [String] = [] + var propKinds: [Int32] = [] + var propStrings: [String] = [] + var propBits: [Int64] = [] for (key, value) in node.props { + let slot = slots(for: value) propKeys.append(key) - propValues.append(jsonLiteral(value)) + propKinds.append(slot.kind) + propStrings.append(slot.string) + propBits.append(slot.bits) } var modifierKinds: [String] = [] - var modifierArgs: [String] = [] + var modifierArgCounts: [Int32] = [] + var argKeys: [String] = [] + var argKinds: [Int32] = [] + var argStrings: [String] = [] + var argBits: [Int64] = [] for modifier in node.modifiers { modifierKinds.append(modifier.kind) - modifierArgs.append(jsonObjectLiteral(modifier.args)) + modifierArgCounts.append(Int32(modifier.args.count)) + for (key, value) in modifier.args { + let slot = slots(for: value) + argKeys.append(key) + argKinds.append(slot.kind) + argStrings.append(slot.string) + argBits.append(slot.bits) + } } let children = node.children.map { materialize($0) as ViewNodeObject? } return ViewNodeObject( node.type, node.id, propKeys, - propValues, + propKinds, + propStrings, + propBits, modifierKinds, - modifierArgs, + modifierArgCounts, + argKeys, + argKinds, + argStrings, + argBits, children, Int32(node.count ?? -1), Int64(-1)