mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
feat(examples): iOS example consumes the generated Swift wrapper
Regenerates Sources/MyTimer/MyTimer.swift via `nimble genbindings_swift` (replacing the hand-written wrapper) and points the tests at the derived MyTimerNode class. Proves the generator reproduces the validated create/version/echo path: build-xcframework.sh + `swift test` pass 2/2 on the macOS slice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b7fa33f2c7
commit
c43072da46
@ -1,9 +1,10 @@
|
||||
// Idiomatic Swift wrapper over the timer library's native C ABI.
|
||||
// Generated by nim-ffi Swift codegen. Do not edit by hand.
|
||||
//
|
||||
// Each call is dispatched on the library's background FFI thread and its result
|
||||
// arrives on a callback; we bridge that to a synchronous Swift API with a
|
||||
// semaphore. A struct return (EchoResponse) is read out of the typed C-POD
|
||||
// inside the callback — it is valid only for the callback's lifetime.
|
||||
// Idiomatic Swift wrapper over the library's native (zero-serialization) C ABI.
|
||||
// Each call is dispatched on the library's background FFI thread; we block on a
|
||||
// DispatchSemaphore until the result callback fires. A struct return is read out
|
||||
// of the typed C-POD inside the callback — valid only for the callback's
|
||||
// lifetime — and copied into a native Swift value.
|
||||
import CMyTimer
|
||||
import Foundation
|
||||
|
||||
@ -14,23 +15,22 @@ public enum TimerError: Error, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
public struct EchoResult: Equatable {
|
||||
public struct EchoResponse: Equatable {
|
||||
public let echoed: String
|
||||
public let timerName: String
|
||||
}
|
||||
|
||||
public final class TimerNode {
|
||||
public final class MyTimerNode {
|
||||
private let ctx: UnsafeMutableRawPointer
|
||||
|
||||
/// Creates the timer context (TimerConfig by value).
|
||||
public init(name: String) throws {
|
||||
let box = Box()
|
||||
let ud = Unmanaged.passUnretained(box).toOpaque()
|
||||
let cName = strdup(name)
|
||||
defer { free(cName) }
|
||||
var cfg = TimerConfig()
|
||||
cfg.name = UnsafePointer(cName)
|
||||
guard let c = my_timer_create(cfg, ackCallback, ud) else {
|
||||
var c_config = CMyTimer.TimerConfig()
|
||||
let c_config_name = strdup(name)
|
||||
defer { free(c_config_name) }
|
||||
c_config.name = UnsafePointer(c_config_name)
|
||||
guard let c = my_timer_create(c_config, ackCallback, ud) else {
|
||||
throw TimerError.failed("create returned null")
|
||||
}
|
||||
box.sem.wait()
|
||||
@ -38,7 +38,22 @@ public final class TimerNode {
|
||||
ctx = c
|
||||
}
|
||||
|
||||
/// String-returning call: the raw bytes are the version string.
|
||||
public func echo(_ message: String, delayMs: Int = 0) throws -> EchoResponse {
|
||||
let box = MyTimerEchoBox()
|
||||
let ud = Unmanaged.passUnretained(box).toOpaque()
|
||||
var c_req = CMyTimer.EchoRequest()
|
||||
let c_req_message = strdup(message)
|
||||
defer { free(c_req_message) }
|
||||
c_req.message = UnsafePointer(c_req_message)
|
||||
c_req.delayMs = Int64(delayMs)
|
||||
guard my_timer_echo(ctx, my_timer_echoCallback, ud, c_req) == 0 else {
|
||||
throw TimerError.failed("echo dispatch failed")
|
||||
}
|
||||
box.sem.wait()
|
||||
guard box.ret == 0 else { throw TimerError.failed(box.text) }
|
||||
return EchoResponse(echoed: box.echoed, timerName: box.timerName)
|
||||
}
|
||||
|
||||
public func version() throws -> String {
|
||||
let box = Box()
|
||||
let ud = Unmanaged.passUnretained(box).toOpaque()
|
||||
@ -50,73 +65,53 @@ public final class TimerNode {
|
||||
return box.text
|
||||
}
|
||||
|
||||
/// Struct param in, typed struct (EchoResponse) out.
|
||||
public func echo(_ message: String, delayMs: Int = 0) throws -> EchoResult {
|
||||
let box = EchoBox()
|
||||
let ud = Unmanaged.passUnretained(box).toOpaque()
|
||||
let cMsg = strdup(message)
|
||||
defer { free(cMsg) }
|
||||
var req = EchoRequest()
|
||||
req.message = UnsafePointer(cMsg)
|
||||
req.delayMs = Int64(delayMs)
|
||||
guard my_timer_echo(ctx, echoCallback, ud, req) == 0 else {
|
||||
throw TimerError.failed("echo dispatch failed")
|
||||
}
|
||||
box.sem.wait()
|
||||
guard box.ret == 0 else { throw TimerError.failed(box.text) }
|
||||
return EchoResult(echoed: box.echoed, timerName: box.timerName)
|
||||
}
|
||||
|
||||
deinit { my_timer_destroy(ctx) }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - callback plumbing
|
||||
// The library calls back on its FFI thread; we keep the Box alive on the caller
|
||||
// stack (passUnretained) because the caller blocks on the semaphore until the
|
||||
// callback fires.
|
||||
|
||||
// MARK: - shared callback plumbing
|
||||
final class Box {
|
||||
var ret: Int32 = -1
|
||||
var text = ""
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
}
|
||||
final class EchoBox {
|
||||
var ret: Int32 = -1
|
||||
var text = ""
|
||||
var echoed = ""
|
||||
var timerName = ""
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
}
|
||||
|
||||
private func rawText(_ msg: UnsafePointer<CChar>?, _ len: Int) -> String {
|
||||
func rawText(_ msg: UnsafePointer<CChar>?, _ len: Int) -> String {
|
||||
guard let m = msg, len > 0 else { return "" }
|
||||
let bytes = UnsafeRawPointer(m).assumingMemoryBound(to: UInt8.self)
|
||||
return String(decoding: UnsafeBufferPointer(start: bytes, count: len), as: UTF8.self)
|
||||
}
|
||||
|
||||
private func ackCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
|
||||
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
|
||||
func ackCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
|
||||
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
|
||||
let box = Unmanaged<Box>.fromOpaque(ud!).takeUnretainedValue()
|
||||
box.ret = ret
|
||||
if ret != 0 { box.text = rawText(msg, len) }
|
||||
box.sem.signal()
|
||||
}
|
||||
|
||||
private func stringCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
|
||||
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
|
||||
func stringCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
|
||||
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
|
||||
let box = Unmanaged<Box>.fromOpaque(ud!).takeUnretainedValue()
|
||||
box.ret = ret
|
||||
box.text = rawText(msg, len)
|
||||
box.sem.signal()
|
||||
}
|
||||
|
||||
private func echoCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
|
||||
|
||||
final class MyTimerEchoBox {
|
||||
var ret: Int32 = -1
|
||||
var text = ""
|
||||
var echoed = ""
|
||||
var timerName = ""
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
}
|
||||
private func my_timer_echoCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
|
||||
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
|
||||
let box = Unmanaged<EchoBox>.fromOpaque(ud!).takeUnretainedValue()
|
||||
let box = Unmanaged<MyTimerEchoBox>.fromOpaque(ud!).takeUnretainedValue()
|
||||
box.ret = ret
|
||||
if ret == 0, let m = msg {
|
||||
// Native ABI: msg is a const EchoResponse* (typed struct return).
|
||||
let resp = UnsafeRawPointer(m).assumingMemoryBound(to: EchoResponse.self)
|
||||
let resp = UnsafeRawPointer(m).assumingMemoryBound(to: CMyTimer.EchoResponse.self)
|
||||
box.echoed = resp.pointee.echoed.map { String(cString: $0) } ?? ""
|
||||
box.timerName = resp.pointee.timerName.map { String(cString: $0) } ?? ""
|
||||
} else {
|
||||
|
||||
@ -3,7 +3,7 @@ import XCTest
|
||||
|
||||
final class MyTimerTests: XCTestCase {
|
||||
func testCreateVersionEcho() throws {
|
||||
let node = try TimerNode(name: "ios-demo")
|
||||
let node = try MyTimerNode(name: "ios-demo")
|
||||
|
||||
XCTAssertEqual(try node.version(), "nim-timer v0.1.0")
|
||||
|
||||
@ -13,7 +13,7 @@ final class MyTimerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testManyEchoes() throws {
|
||||
let node = try TimerNode(name: "loop")
|
||||
let node = try MyTimerNode(name: "loop")
|
||||
for i in 0..<200 {
|
||||
let r = try node.echo("m\(i)")
|
||||
XCTAssertEqual(r.echoed, "m\(i)")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user