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
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,15 @@ import SharedCore
extension Data {

/// Kotlin's `ByteArray` has no `Data` bridge of its own, so every exported function taking
/// bytes needs this copy.
/// bytes needs this copy. It goes through `NSData` because the only element-wise alternative,
/// `KotlinByteArray.set(index:value:)`, costs one Objective-C message send per byte.
var kotlinByteArray: KotlinByteArray {
let array = KotlinByteArray(size: Int32(count))
for (offset, byte) in enumerated() {
array.set(index: Int32(offset), value: Int8(bitPattern: byte))
}
return array
SharedBytes.shared.byteArray(data: self)
}

/// The reverse copy, for the exported functions that hand bytes back. Kotlin bytes are signed,
/// so anything above 0x7F comes across negative and has to be reinterpreted rather than
/// converted.
/// but a bulk copy moves the bit patterns unchanged, so nothing has to be reinterpreted here.
init(_ array: KotlinByteArray) {
var bytes = [UInt8]()
bytes.reserveCapacity(Int(array.size))
for index in 0..<array.size {
bytes.append(UInt8(bitPattern: array.get(index: index)))
}
self.init(bytes)
self = SharedBytes.shared.data(bytes: array)
}
}
22 changes: 22 additions & 0 deletions kmp/shared-core/spm/Tests/SharedCoreKitTests/DataBridgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,25 @@ import SharedCore
#expect(Data(original.kotlinByteArray) == original)
}
}

@Suite struct DataBridgeThroughputTests {

/// The size of the two discrete-curve tables the iOS app hands the bridge on first use.
private static let curveTableByteCount = 6_720_032

/// A per-element bridge costs one Objective-C message send per byte, which put ~1.9s of the
/// iOS app's launch inside `SharedBondingCurve.initialize`. The budget is two orders of
/// magnitude above a bulk copy of this size and an order of magnitude below the per-element
/// cost, so it fails on a regression rather than on a slow machine.
@Test func convertsCurveSizedDataWithinBudget() {
let original = Data(repeating: 0xAB, count: Self.curveTableByteCount)

let start = Date()
let kotlin = original.kotlinByteArray
let round = Data(kotlin)
let elapsed = Date().timeIntervalSince(start)

#expect(round.count == original.count)
#expect(elapsed < 0.2, "round trip of \(Self.curveTableByteCount) bytes took \(elapsed)s")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.flipcash.shared

import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.Foundation.NSData
import platform.Foundation.create
import platform.posix.memcpy

/**
* Bulk conversions between Foundation's `NSData` and Kotlin's `ByteArray`.
*
* Swift callers have no other way to hand a `ByteArray` its contents than `set(index:value:)`,
* which is one Objective-C message send per byte. Copying through `NSData` instead moves the
* whole buffer in a single `memcpy`, which matters for anything larger than a key or a hash —
* the discrete-curve tables are 6.7 MB.
*/
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
object SharedBytes {

/** Copies [data]'s bytes into a new `ByteArray`. */
fun byteArray(data: NSData): ByteArray {
val size = data.length.toInt()
if (size == 0) return ByteArray(0)
val bytes = ByteArray(size)
bytes.usePinned { memcpy(it.addressOf(0), data.bytes, data.length) }
return bytes
}

/** Copies [bytes] into a new `NSData`. */
fun data(bytes: ByteArray): NSData {
if (bytes.isEmpty()) return NSData()
return bytes.usePinned {
NSData.create(bytes = it.addressOf(0), length = bytes.size.toULong())
}
}
}
Loading