From c43072da46ae0d4a7e05cfec8e02fc2e711d9b63 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sat, 13 Jun 2026 16:40:04 +0200 Subject: [PATCH] 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 --- .../timer/ios/Sources/MyTimer/MyTimer.swift | 99 +++++++++---------- .../ios/Tests/MyTimerTests/MyTimerTests.swift | 4 +- 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/examples/timer/ios/Sources/MyTimer/MyTimer.swift b/examples/timer/ios/Sources/MyTimer/MyTimer.swift index daadfdd..33fe74e 100644 --- a/examples/timer/ios/Sources/MyTimer/MyTimer.swift +++ b/examples/timer/ios/Sources/MyTimer/MyTimer.swift @@ -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?, _ len: Int) -> String { +func rawText(_ msg: UnsafePointer?, _ 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?, - _ len: Int, _ ud: UnsafeMutableRawPointer?) { +func ackCallback(_ ret: Int32, _ msg: UnsafePointer?, + _ len: Int, _ ud: UnsafeMutableRawPointer?) { let box = Unmanaged.fromOpaque(ud!).takeUnretainedValue() box.ret = ret if ret != 0 { box.text = rawText(msg, len) } box.sem.signal() } -private func stringCallback(_ ret: Int32, _ msg: UnsafePointer?, - _ len: Int, _ ud: UnsafeMutableRawPointer?) { +func stringCallback(_ ret: Int32, _ msg: UnsafePointer?, + _ len: Int, _ ud: UnsafeMutableRawPointer?) { let box = Unmanaged.fromOpaque(ud!).takeUnretainedValue() box.ret = ret box.text = rawText(msg, len) box.sem.signal() } -private func echoCallback(_ ret: Int32, _ msg: UnsafePointer?, + +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?, _ len: Int, _ ud: UnsafeMutableRawPointer?) { - let box = Unmanaged.fromOpaque(ud!).takeUnretainedValue() + let box = Unmanaged.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 { diff --git a/examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift b/examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift index e2f4f4f..195306e 100644 --- a/examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift +++ b/examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift @@ -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)")