Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Sources/ComposeUI/KotlinBindings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import SwiftJava
open class ViewNodeObject: JavaObject {

/// The bridge constructor: one JNI call per node, arrays crossing as
/// 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
/// single arguments. Every value crosses typed — a kind tag per value
/// selects the string slot or the bits slot (doubles bit-cast into the
/// long). Homogeneous arrays ride the same slot with the bits packing
/// (offset << 32 | count) into the node's shared string or long pool; only
/// nested/heterogeneous arrays take the JSON path. Negative count/provider
/// mean "absent".
@JavaMethod
@_nonoverride public convenience init(
Expand All @@ -32,6 +34,8 @@ open class ViewNodeObject: JavaObject {
_ argKinds: [Int32],
_ argStrings: [String],
_ argBits: [Int64],
_ stringPool: [String],
_ longPool: [Int64],
_ children: [ViewNodeObject?],
_ count: Int32,
_ itemProviderId: Int64,
Expand Down
90 changes: 75 additions & 15 deletions Sources/ComposeUI/Materializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,37 @@ import SwiftJava

public enum Materializer {

// value kinds understood by the Kotlin bridge constructor
// scalar 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
// homogeneous-array kinds: the bits slot packs (offset << 32 | count) into
// the node's shared string or long pool
private static let kindStringArray: Int32 = 5
private static let kindDoubleArray: Int32 = 6
private static let kindBoolArray: Int32 = 7
private static let kindIntArray: Int32 = 8

/// 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) {
/// One node's array pools, filled while its values are encoded and handed
/// to the constructor. Homogeneous arrays append their elements here and
/// carry a packed (offset, count) reference in place of a JSON string.
private struct Pools {
var strings: [String] = []
var longs: [Int64] = []
}

private static func pack(offset: Int, count: Int) -> Int64 {
(Int64(offset) << 32) | Int64(count)
}

/// One value'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); a homogeneous array appends to a pool and packs its
/// (offset, count) into the bits slot; only a nested/mixed array falls back
/// to JSON text.
private static func slots(for value: PropValue, pools: inout Pools) -> (kind: Int32, string: String, bits: Int64) {
switch value {
case .string(let string):
return (kindString, string, 0)
Expand All @@ -31,19 +51,60 @@ public enum Materializer {
return (kindBool, "", bool ? 1 : 0)
case .int(let int):
return (kindInt, "", Int64(int))
case .array:
return (kindJSON, jsonLiteral(value), 0)
case .array(let elements):
return arraySlot(elements, value: value, pools: &pools)
}
}

/// Routes an array to a typed pool when its elements share one scalar kind;
/// otherwise (nested or mixed — alert `buttons`, `searches`) to JSON.
private static func arraySlot(
_ elements: [PropValue],
value: PropValue,
pools: inout Pools
) -> (kind: Int32, string: String, bits: Int64) {
func mapAll<T>(_ transform: (PropValue) -> T?) -> [T]? {
var out: [T] = []
out.reserveCapacity(elements.count)
for element in elements {
guard let mapped = transform(element) else { return nil }
out.append(mapped)
}
return out
}
guard !elements.isEmpty else { return (kindJSON, "[]", 0) }
if let strings = mapAll({ if case let .string(s) = $0 { s } else { nil } }) {
let bits = pack(offset: pools.strings.count, count: strings.count)
pools.strings.append(contentsOf: strings)
return (kindStringArray, "", bits)
}
if let ints = mapAll({ if case let .int(i) = $0 { Int64(i) } else { nil } }) {
let bits = pack(offset: pools.longs.count, count: ints.count)
pools.longs.append(contentsOf: ints)
return (kindIntArray, "", bits)
}
if let doubles = mapAll({ if case let .double(d) = $0 { Int64(bitPattern: d.bitPattern) } else { nil } }) {
let bits = pack(offset: pools.longs.count, count: doubles.count)
pools.longs.append(contentsOf: doubles)
return (kindDoubleArray, "", bits)
}
if let bools = mapAll({ if case let .bool(b) = $0 { Int64(b ? 1 : 0) } else { nil } }) {
let bits = pack(offset: pools.longs.count, count: bools.count)
pools.longs.append(contentsOf: bools)
return (kindBoolArray, "", bits)
}
return (kindJSON, jsonLiteral(value), 0)
}

/// Builds the Kotlin mirror of an IR tree, depth-first.
public static func materialize(_ node: RenderNode) -> ViewNodeObject {
var pools = Pools()
var propKeys: [String] = []
var propKinds: [Int32] = []
var propStrings: [String] = []
var propBits: [Int64] = []
for (key, value) in node.props {
let slot = slots(for: value)
let slot = slots(for: value, pools: &pools)
propKeys.append(key)
propKinds.append(slot.kind)
propStrings.append(slot.string)
Expand All @@ -59,7 +120,7 @@ public enum Materializer {
modifierKinds.append(modifier.kind)
modifierArgCounts.append(Int32(modifier.args.count))
for (key, value) in modifier.args {
let slot = slots(for: value)
let slot = slots(for: value, pools: &pools)
argKeys.append(key)
argKinds.append(slot.kind)
argStrings.append(slot.string)
Expand All @@ -80,13 +141,17 @@ public enum Materializer {
argKinds,
argStrings,
argBits,
pools.strings,
pools.longs,
children,
Int32(node.count ?? -1),
Int64(-1)
)
}

/// Encodes a prop value as a JSON literal (`"text"`, `42`, `true`, `[…]`).
/// Encodes an array-valued prop as a JSON literal (`["a",1,true]`), the one
/// value shape the typed slots don't carry. Scalars never reach this — they
/// cross typed — so the only recursion is through nested array elements.
static func jsonLiteral(_ value: PropValue) -> String {
switch value {
case .string(let string):
Expand All @@ -102,11 +167,6 @@ public enum Materializer {
}
}

static func jsonObjectLiteral(_ args: [String: PropValue]) -> String {
let members = args.map { "\(escapeJSON($0.key)):\(jsonLiteral($0.value))" }
return "{" + members.joined(separator: ",") + "}"
}

/// Minimal JSON string escaping (quotes, backslashes, control characters).
static func escapeJSON(_ string: String) -> String {
var out = "\""
Expand Down
53 changes: 41 additions & 12 deletions composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.pureswift.swiftui
import androidx.compose.runtime.Immutable
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
Expand Down Expand Up @@ -43,12 +44,15 @@ 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). 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".
/// arguments). Every value crosses typed — a kind tag selects the string
/// slot or the bits slot (doubles bit-cast into the long, so both 64-bit
/// callback ids and doubles cross exactly). Homogeneous arrays ride the
/// same slot: the kind marks the element type and the bits slot packs
/// (offset << 32 | count) into the node's shared string or long pool. Only
/// a heterogeneous/nested array (alert `buttons`, `searches`) still crosses
/// as a JSON literal. Modifier args flatten into one run of slots,
/// `modifierArgCounts` giving each modifier's share. Negative count/provider
/// mean "absent".
constructor(
type: String,
id: String,
Expand All @@ -62,22 +66,24 @@ data class ViewNode(
argKinds: IntArray,
argStrings: Array<String>,
argBits: LongArray,
stringPool: Array<String>,
longPool: LongArray,
children: Array<ViewNode>,
count: Int,
itemProviderId: Long,
) : this(
type = type,
id = id,
props = JsonObject(propKeys.indices.associate {
propKeys[it] to jsonValue(propKinds[it], propStrings[it], propBits[it])
propKeys[it] to jsonValue(propKinds[it], propStrings[it], propBits[it], stringPool, longPool)
}),
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])
argKeys[i] to jsonValue(argKinds[i], argStrings[i], argBits[i], stringPool, longPool)
})))
base += argCount
}
Expand All @@ -88,20 +94,43 @@ data class ViewNode(
)

companion object {
// value kinds used by the bridge constructor
// scalar value kinds
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 =
// homogeneous-array kinds: bits packs (offset << 32 | count) into a pool
private const val KIND_STRING_ARRAY = 5
private const val KIND_DOUBLE_ARRAY = 6
private const val KIND_BOOL_ARRAY = 7
private const val KIND_INT_ARRAY = 8

private fun jsonValue(
kind: Int,
string: String,
bits: Long,
stringPool: Array<String>,
longPool: LongArray,
): 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
KIND_JSON -> Json.parseToJsonElement(string) // nested/mixed arrays only
else -> {
val offset = (bits ushr 32).toInt()
val count = (bits and 0xFFFFFFFFL).toInt()
JsonArray((0 until count).map { i ->
when (kind) {
KIND_STRING_ARRAY -> JsonPrimitive(stringPool[offset + i])
KIND_DOUBLE_ARRAY -> JsonPrimitive(Double.fromBits(longPool[offset + i]))
KIND_BOOL_ARRAY -> JsonPrimitive(longPool[offset + i] != 0L)
else -> JsonPrimitive(longPool[offset + i]) // KIND_INT_ARRAY
}
})
}
}
}

Expand Down
Loading