mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-26 01:23:17 +00:00
Merge c43072da46ae0d4a7e05cfec8e02fc2e711d9b63 into c000a8467dfc81af043bbb1f11d1da03570e5128
This commit is contained in:
commit
44ce03fd1e
4
examples/timer/ios/.gitignore
vendored
Normal file
4
examples/timer/ios/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/MyTimer.xcframework/
|
||||
/.build-slices/
|
||||
/.build/
|
||||
*.o
|
||||
22
examples/timer/ios/Package.swift
Normal file
22
examples/timer/ios/Package.swift
Normal file
@ -0,0 +1,22 @@
|
||||
// swift-tools-version:5.9
|
||||
import PackageDescription
|
||||
|
||||
// SwiftPM package wrapping the timer library for iOS (and macOS, so the Swift
|
||||
// wrapper is testable on the host with `swift test`).
|
||||
//
|
||||
// `MyTimer.xcframework` is produced by ./build-xcframework.sh and bundles the
|
||||
// static library for ios-arm64 (device), ios-arm64-simulator, and macos-arm64,
|
||||
// each with the C headers + module map. Run the build script before
|
||||
// `swift build` / `swift test`.
|
||||
let package = Package(
|
||||
name: "MyTimer",
|
||||
platforms: [.iOS(.v13), .macOS(.v12)],
|
||||
products: [
|
||||
.library(name: "MyTimer", targets: ["MyTimer"])
|
||||
],
|
||||
targets: [
|
||||
.binaryTarget(name: "CMyTimer", path: "MyTimer.xcframework"),
|
||||
.target(name: "MyTimer", dependencies: ["CMyTimer"]),
|
||||
.testTarget(name: "MyTimerTests", dependencies: ["MyTimer"]),
|
||||
]
|
||||
)
|
||||
55
examples/timer/ios/README.md
Normal file
55
examples/timer/ios/README.md
Normal file
@ -0,0 +1,55 @@
|
||||
# iOS example — Swift over the native C ABI
|
||||
|
||||
A SwiftPM package that wraps the timer library's **native** (zero-serialization)
|
||||
C ABI behind an idiomatic Swift class, `TimerNode`. The library is cross-compiled
|
||||
to a static `.xcframework` and consumed from Swift; struct returns come back as
|
||||
typed Swift values.
|
||||
|
||||
```swift
|
||||
let node = try TimerNode(name: "my-app")
|
||||
print(try node.version()) // "nim-timer v0.1.0"
|
||||
let r = try node.echo("hello", delayMs: 5) // EchoResult
|
||||
print(r.echoed, r.timerName)
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `build-xcframework.sh` | Cross-compiles the Nim lib to `MyTimer.xcframework` (ios-arm64 device, ios-arm64 simulator, macos-arm64) and bundles the C headers + module map. |
|
||||
| `cheaders/` | The native C header (`my_timer.h`), CBOR header, and `module.modulemap` (module `CMyTimer`). |
|
||||
| `Sources/MyTimer/MyTimer.swift` | The Swift wrapper. Bridges the async FFI-thread callback to a synchronous Swift API with a semaphore; reads the typed `EchoResponse` struct out of the callback. |
|
||||
| `Package.swift` | SwiftPM: a binary target (the xcframework) + the Swift wrapper + tests. |
|
||||
| `Tests/` | Unit tests, runnable on the host via the macOS slice. |
|
||||
|
||||
## Build & test
|
||||
|
||||
```sh
|
||||
cd examples/timer/ios
|
||||
./build-xcframework.sh # builds the 3 slices into MyTimer.xcframework
|
||||
swift test # runs on the host (macos-arm64 slice)
|
||||
```
|
||||
|
||||
`build-xcframework.sh` assembles the `.xcframework` directly (no
|
||||
`xcodebuild -create-xcframework`), so it works in headless / CI environments.
|
||||
The **macos-arm64** slice exists only so the wrapper is testable with
|
||||
`swift test`; real iOS deployment uses the **ios-arm64** (device) and
|
||||
**ios-arm64-simulator** slices.
|
||||
|
||||
## Use it in an iOS app
|
||||
|
||||
1. Run `./build-xcframework.sh`.
|
||||
2. Add this directory as a local Swift package (Xcode → *Add Package
|
||||
Dependencies… → Add Local…*), or depend on it in your own `Package.swift`.
|
||||
3. `import MyTimer` and use `TimerNode`.
|
||||
|
||||
## Notes
|
||||
|
||||
- This is the **native, same-process** path — the app links the library directly.
|
||||
The CBOR ABI is for inter-process communication only (see [`../ipc`](../ipc)).
|
||||
- Each call is dispatched on the library's background FFI thread; `TimerNode`
|
||||
blocks on a semaphore until the result callback fires. A struct return (e.g.
|
||||
`EchoResponse`) is read inside the callback — it is valid only for the
|
||||
callback's lifetime — and copied into the returned Swift value.
|
||||
- Regenerate `cheaders/my_timer.h` from the repo root with `nimble genbindings_c`
|
||||
if the library's API changes.
|
||||
121
examples/timer/ios/Sources/MyTimer/MyTimer.swift
Normal file
121
examples/timer/ios/Sources/MyTimer/MyTimer.swift
Normal file
@ -0,0 +1,121 @@
|
||||
// Generated by nim-ffi Swift codegen. Do not edit by hand.
|
||||
//
|
||||
// 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
|
||||
|
||||
public enum TimerError: Error, CustomStringConvertible {
|
||||
case failed(String)
|
||||
public var description: String {
|
||||
switch self { case let .failed(m): return m }
|
||||
}
|
||||
}
|
||||
|
||||
public struct EchoResponse: Equatable {
|
||||
public let echoed: String
|
||||
public let timerName: String
|
||||
}
|
||||
|
||||
public final class MyTimerNode {
|
||||
private let ctx: UnsafeMutableRawPointer
|
||||
|
||||
public init(name: String) throws {
|
||||
let box = Box()
|
||||
let ud = Unmanaged.passUnretained(box).toOpaque()
|
||||
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()
|
||||
guard box.ret == 0 else { throw TimerError.failed(box.text) }
|
||||
ctx = c
|
||||
}
|
||||
|
||||
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()
|
||||
guard my_timer_version(ctx, stringCallback, ud) == 0 else {
|
||||
throw TimerError.failed("version dispatch failed")
|
||||
}
|
||||
box.sem.wait()
|
||||
guard box.ret == 0 else { throw TimerError.failed(box.text) }
|
||||
return box.text
|
||||
}
|
||||
|
||||
deinit { my_timer_destroy(ctx) }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - shared callback plumbing
|
||||
final class Box {
|
||||
var ret: Int32 = -1
|
||||
var text = ""
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
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<MyTimerEchoBox>.fromOpaque(ud!).takeUnretainedValue()
|
||||
box.ret = ret
|
||||
if ret == 0, let m = msg {
|
||||
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 {
|
||||
box.text = rawText(msg, len)
|
||||
}
|
||||
box.sem.signal()
|
||||
}
|
||||
22
examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift
Normal file
22
examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift
Normal file
@ -0,0 +1,22 @@
|
||||
import XCTest
|
||||
@testable import MyTimer
|
||||
|
||||
final class MyTimerTests: XCTestCase {
|
||||
func testCreateVersionEcho() throws {
|
||||
let node = try MyTimerNode(name: "ios-demo")
|
||||
|
||||
XCTAssertEqual(try node.version(), "nim-timer v0.1.0")
|
||||
|
||||
let r = try node.echo("hello from Swift", delayMs: 2)
|
||||
XCTAssertEqual(r.echoed, "hello from Swift")
|
||||
XCTAssertEqual(r.timerName, "ios-demo") // proves the lib's own state round-tripped
|
||||
}
|
||||
|
||||
func testManyEchoes() throws {
|
||||
let node = try MyTimerNode(name: "loop")
|
||||
for i in 0..<200 {
|
||||
let r = try node.echo("m\(i)")
|
||||
XCTAssertEqual(r.echoed, "m\(i)")
|
||||
}
|
||||
}
|
||||
}
|
||||
96
examples/timer/ios/build-xcframework.sh
Executable file
96
examples/timer/ios/build-xcframework.sh
Executable file
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build MyTimer.xcframework from the Nim timer library:
|
||||
# - ios-arm64 (device)
|
||||
# - ios-arm64-simulator (Apple-silicon simulator)
|
||||
# - macos-arm64 (so the Swift wrapper is testable with `swift test`)
|
||||
#
|
||||
# Each slice is a static library cross-compiled by Nim with the matching SDK,
|
||||
# then bundled with the C headers + module map. Requires Xcode + Nim.
|
||||
#
|
||||
# ./build-xcframework.sh
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$HERE/../../.." && pwd)"
|
||||
NIM_SRC="$REPO_ROOT/examples/timer/timer.nim"
|
||||
STAGE="$HERE/.build-slices"
|
||||
OUT="$HERE/MyTimer.xcframework"
|
||||
|
||||
CLANG="$(xcrun -f clang)"
|
||||
COMMON_NIM=(--mm:orc -d:release -d:chronicles_log_level=WARN --threads:on
|
||||
--os:macosx --cpu:arm64 --app:staticlib --noMain
|
||||
--nimMainPrefix:libmy_timer --cc:clang
|
||||
"--clang.exe:$CLANG" "--clang.linkerexe:$CLANG")
|
||||
|
||||
# build_slice <name> <sdk> <min-flag>
|
||||
build_slice() {
|
||||
local name="$1" sdk="$2" minflag="$3"
|
||||
local sysroot; sysroot="$(xcrun --sdk "$sdk" --show-sdk-path)"
|
||||
local dir="$STAGE/$name"
|
||||
mkdir -p "$dir"
|
||||
echo ">> building slice: $name ($sdk)"
|
||||
( cd "$REPO_ROOT" && nim c "${COMMON_NIM[@]}" \
|
||||
--nimcache:"$dir/nimcache" \
|
||||
--passC:"-isysroot $sysroot -arch arm64 $minflag" \
|
||||
--passL:"-isysroot $sysroot -arch arm64 $minflag" \
|
||||
-o:"$dir/libmy_timer.a" "$NIM_SRC" >/dev/null )
|
||||
}
|
||||
|
||||
rm -rf "$STAGE" "$OUT"
|
||||
build_slice device iphoneos "-miphoneos-version-min=13.0"
|
||||
build_slice simulator iphonesimulator "-mios-simulator-version-min=13.0"
|
||||
build_slice macos macosx "-mmacosx-version-min=12.0"
|
||||
|
||||
echo ">> assembling $OUT"
|
||||
# Assemble the .xcframework by hand (a directory + Info.plist) rather than via
|
||||
# `xcodebuild -create-xcframework` — same on-disk format, but no dependency on a
|
||||
# working Simulator toolchain, so it builds in headless / CI environments too.
|
||||
add_slice() { # <stage-name> <library-identifier>
|
||||
local dir="$OUT/$2"
|
||||
mkdir -p "$dir/Headers"
|
||||
cp "$STAGE/$1/libmy_timer.a" "$dir/libmy_timer.a"
|
||||
cp "$HERE/cheaders/"* "$dir/Headers/"
|
||||
}
|
||||
mkdir -p "$OUT"
|
||||
add_slice device ios-arm64
|
||||
add_slice simulator ios-arm64-simulator
|
||||
add_slice macos macos-arm64
|
||||
|
||||
cat > "$OUT/Info.plist" <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key><string>ios-arm64</string>
|
||||
<key>LibraryPath</key><string>libmy_timer.a</string>
|
||||
<key>HeadersPath</key><string>Headers</string>
|
||||
<key>SupportedArchitectures</key><array><string>arm64</string></array>
|
||||
<key>SupportedPlatform</key><string>ios</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key><string>ios-arm64-simulator</string>
|
||||
<key>LibraryPath</key><string>libmy_timer.a</string>
|
||||
<key>HeadersPath</key><string>Headers</string>
|
||||
<key>SupportedArchitectures</key><array><string>arm64</string></array>
|
||||
<key>SupportedPlatform</key><string>ios</string>
|
||||
<key>SupportedPlatformVariant</key><string>simulator</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key><string>macos-arm64</string>
|
||||
<key>LibraryPath</key><string>libmy_timer.a</string>
|
||||
<key>HeadersPath</key><string>Headers</string>
|
||||
<key>SupportedArchitectures</key><array><string>arm64</string></array>
|
||||
<key>SupportedPlatform</key><string>macos</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key><string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key><string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
echo ">> done. Slices:"
|
||||
ls "$OUT"
|
||||
5
examples/timer/ios/cheaders/module.modulemap
Normal file
5
examples/timer/ios/cheaders/module.modulemap
Normal file
@ -0,0 +1,5 @@
|
||||
module CMyTimer {
|
||||
header "my_timer.h"
|
||||
header "my_timer_cbor.h"
|
||||
export *
|
||||
}
|
||||
121
examples/timer/ios/cheaders/my_timer.h
Normal file
121
examples/timer/ios/cheaders/my_timer.h
Normal file
@ -0,0 +1,121 @@
|
||||
// Generated by nim-ffi C codegen. Do not edit by hand.
|
||||
//
|
||||
// Native (zero-serialization) C ABI. Each call delivers its result to the
|
||||
// callback. On RET_OK:
|
||||
// - string-returning procs: (msg, len) is the raw string bytes (not
|
||||
// NUL-terminated; use len).
|
||||
// - struct-returning procs: msg is a pointer to the returned C struct — cast
|
||||
// it to `const <Type>*` (len is sizeof). It is valid ONLY for the duration
|
||||
// of the callback; copy out anything you need before returning. The library
|
||||
// deep-frees it right after the callback (you free nothing).
|
||||
// On RET_ERR, (msg, len) is the raw error text. A `<name>_cbor` variant of each
|
||||
// proc also exists for generic/cross-language callers that prefer CBOR.
|
||||
#ifndef NIM_FFI_GEN_MY_TIMER_H
|
||||
#define NIM_FFI_GEN_MY_TIMER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_RET_CODES
|
||||
#define NIM_FFI_RET_CODES
|
||||
#define RET_OK 0
|
||||
#define RET_ERR 1
|
||||
#define RET_MISSING_CALLBACK 2
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_CALLBACK_T
|
||||
#define NIM_FFI_CALLBACK_T
|
||||
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
|
||||
#endif
|
||||
|
||||
|
||||
// --- {.ffi.}-annotated types, exposed as C structs ----------
|
||||
typedef struct {
|
||||
const char* name;
|
||||
} TimerConfig;
|
||||
|
||||
typedef struct {
|
||||
const char* message;
|
||||
int64_t delayMs;
|
||||
} EchoRequest;
|
||||
|
||||
typedef struct {
|
||||
const char* echoed;
|
||||
const char* timerName;
|
||||
} EchoResponse;
|
||||
|
||||
typedef struct {
|
||||
EchoRequest *messages;
|
||||
size_t messages_len;
|
||||
const char* *tags;
|
||||
size_t tags_len;
|
||||
int note_present;
|
||||
const char* note;
|
||||
int retries_present;
|
||||
int64_t retries;
|
||||
} ComplexRequest;
|
||||
|
||||
typedef struct {
|
||||
const char* summary;
|
||||
int64_t itemCount;
|
||||
int hasNote;
|
||||
} ComplexResponse;
|
||||
|
||||
typedef struct {
|
||||
const char* message;
|
||||
int64_t echoCount;
|
||||
} EchoEvent;
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
const char* *payload;
|
||||
size_t payload_len;
|
||||
int64_t priority;
|
||||
} JobSpec;
|
||||
|
||||
typedef struct {
|
||||
int64_t maxAttempts;
|
||||
int64_t backoffMs;
|
||||
const char* *retryOn;
|
||||
size_t retryOn_len;
|
||||
} RetryPolicy;
|
||||
|
||||
typedef struct {
|
||||
int64_t startAtMs;
|
||||
int64_t intervalMs;
|
||||
int jitter_present;
|
||||
int64_t jitter;
|
||||
} ScheduleConfig;
|
||||
|
||||
typedef struct {
|
||||
const char* jobId;
|
||||
int64_t willRunCount;
|
||||
int64_t firstRunAtMs;
|
||||
int64_t effectiveBackoffMs;
|
||||
} ScheduleResult;
|
||||
|
||||
|
||||
void *my_timer_create(TimerConfig config, FFICallBack callback, void *userData);
|
||||
|
||||
int my_timer_echo(void *ctx, FFICallBack callback, void *userData, EchoRequest req);
|
||||
|
||||
int my_timer_version(void *ctx, FFICallBack callback, void *userData);
|
||||
|
||||
int my_timer_complex(void *ctx, FFICallBack callback, void *userData, ComplexRequest req);
|
||||
|
||||
int my_timer_schedule(void *ctx, FFICallBack callback, void *userData, JobSpec job, RetryPolicy retry, ScheduleConfig schedule);
|
||||
|
||||
int my_timer_destroy(void *ctx);
|
||||
|
||||
uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);
|
||||
int my_timer_remove_event_listener(void *ctx, uint64_t listenerId);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_GEN_MY_TIMER_H */
|
||||
178
examples/timer/ios/cheaders/my_timer_cbor.h
Normal file
178
examples/timer/ios/cheaders/my_timer_cbor.h
Normal file
@ -0,0 +1,178 @@
|
||||
// Generated by nim-ffi C codegen. Do not edit by hand.
|
||||
//
|
||||
// CBOR ABI (`<name>_cbor`). Use this for callers that cross a process or machine
|
||||
// boundary (the request has to be serialized anyway) or any generic / cross-
|
||||
// language caller. Build the request with the FfiCbor helpers below — a CBOR map
|
||||
// whose keys are the Nim parameter names (listed per proc) — call the matching
|
||||
// `<name>_cbor`, and decode the RET_OK response (a CBOR-encoded value; for
|
||||
// string-returning procs a CBOR text string) with ffi_decode_text. RET_ERR
|
||||
// delivers raw error text. For same-process callers, prefer the native `<name>`
|
||||
// ABI in the companion <lib>.h header.
|
||||
#ifndef NIM_FFI_GEN_MY_TIMER_CBOR_H
|
||||
#define NIM_FFI_GEN_MY_TIMER_CBOR_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_RET_CODES
|
||||
#define NIM_FFI_RET_CODES
|
||||
#define RET_OK 0
|
||||
#define RET_ERR 1
|
||||
#define RET_MISSING_CALLBACK 2
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_CALLBACK_T
|
||||
#define NIM_FFI_CALLBACK_T
|
||||
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_CBOR_HELPERS
|
||||
#define NIM_FFI_CBOR_HELPERS
|
||||
// --- minimal growable CBOR request encoder --------------------------------
|
||||
typedef struct {
|
||||
uint8_t *buf;
|
||||
size_t cap;
|
||||
size_t len;
|
||||
} FfiCbor;
|
||||
|
||||
static inline FfiCbor ffi_cbor_new(void) {
|
||||
FfiCbor c;
|
||||
c.cap = 256;
|
||||
c.len = 0;
|
||||
c.buf = (uint8_t *)malloc(c.cap);
|
||||
return c;
|
||||
}
|
||||
static inline void ffi_cbor_free(FfiCbor *c) {
|
||||
free(c->buf);
|
||||
c->buf = NULL;
|
||||
}
|
||||
static inline void ffi_cbor_put(FfiCbor *c, uint8_t b) {
|
||||
if (c->len >= c->cap) {
|
||||
c->cap *= 2;
|
||||
c->buf = (uint8_t *)realloc(c->buf, c->cap);
|
||||
}
|
||||
c->buf[c->len++] = b;
|
||||
}
|
||||
static inline void ffi_cbor_head(FfiCbor *c, uint8_t major, uint64_t arg) {
|
||||
uint8_t mt = (uint8_t)(major << 5);
|
||||
if (arg < 24) {
|
||||
ffi_cbor_put(c, mt | (uint8_t)arg);
|
||||
} else if (arg <= 0xff) {
|
||||
ffi_cbor_put(c, mt | 24);
|
||||
ffi_cbor_put(c, (uint8_t)arg);
|
||||
} else if (arg <= 0xffff) {
|
||||
ffi_cbor_put(c, mt | 25);
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 8));
|
||||
ffi_cbor_put(c, (uint8_t)arg);
|
||||
} else if (arg <= 0xffffffffULL) {
|
||||
ffi_cbor_put(c, mt | 26);
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 24));
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 16));
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 8));
|
||||
ffi_cbor_put(c, (uint8_t)arg);
|
||||
} else {
|
||||
ffi_cbor_put(c, mt | 27);
|
||||
for (int s = 56; s >= 0; s -= 8) ffi_cbor_put(c, (uint8_t)(arg >> s));
|
||||
}
|
||||
}
|
||||
static inline void ffi_cbor_map(FfiCbor *c, size_t n) { ffi_cbor_head(c, 5, n); }
|
||||
static inline void ffi_cbor_text(FfiCbor *c, const char *s) {
|
||||
size_t n = s ? strlen(s) : 0;
|
||||
ffi_cbor_head(c, 3, n);
|
||||
for (size_t i = 0; i < n; i++) ffi_cbor_put(c, (uint8_t)s[i]);
|
||||
}
|
||||
static inline void ffi_cbor_kv_text(FfiCbor *c, const char *k, const char *v) {
|
||||
ffi_cbor_text(c, k);
|
||||
ffi_cbor_text(c, v);
|
||||
}
|
||||
static inline void ffi_cbor_kv_uint(FfiCbor *c, const char *k, uint64_t v) {
|
||||
ffi_cbor_text(c, k);
|
||||
ffi_cbor_head(c, 0, v);
|
||||
}
|
||||
static inline void ffi_cbor_kv_int(FfiCbor *c, const char *k, int64_t v) {
|
||||
ffi_cbor_text(c, k);
|
||||
if (v >= 0)
|
||||
ffi_cbor_head(c, 0, (uint64_t)v);
|
||||
else
|
||||
ffi_cbor_head(c, 1, (uint64_t)(-(v + 1)));
|
||||
}
|
||||
|
||||
// --- response decoding -----------------------------------------------------
|
||||
// Zero-copy view of a top-level CBOR text string (the RET_OK payload). Sets
|
||||
// *out/*outLen to point INTO `data` (no allocation; valid only while `data` is)
|
||||
// and returns 1; returns 0 for a non-text-string payload.
|
||||
static inline int ffi_text_view(const uint8_t *data, size_t len,
|
||||
const uint8_t **out, size_t *outLen) {
|
||||
if (len < 1 || (data[0] >> 5) != 3) return 0;
|
||||
uint8_t info = data[0] & 0x1f;
|
||||
size_t p = 1;
|
||||
uint64_t slen = 0;
|
||||
if (info < 24) {
|
||||
slen = info;
|
||||
} else if (info == 24) {
|
||||
if (len < p + 1) return 0;
|
||||
slen = data[p++];
|
||||
} else if (info == 25) {
|
||||
if (len < p + 2) return 0;
|
||||
slen = ((uint64_t)data[p] << 8) | data[p + 1];
|
||||
p += 2;
|
||||
} else if (info == 26) {
|
||||
if (len < p + 4) return 0;
|
||||
slen = ((uint64_t)data[p] << 24) | ((uint64_t)data[p + 1] << 16) |
|
||||
((uint64_t)data[p + 2] << 8) | data[p + 3];
|
||||
p += 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (len < p + slen) return 0;
|
||||
*out = data + p;
|
||||
*outLen = (size_t)slen;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Owning variant: malloc a NUL-terminated copy. NULL for a non-text payload.
|
||||
// Caller frees.
|
||||
static inline char *ffi_decode_text(const uint8_t *data, size_t len) {
|
||||
const uint8_t *view;
|
||||
size_t slen;
|
||||
if (!ffi_text_view(data, len, &view, &slen)) return NULL;
|
||||
char *out = (char *)malloc(slen + 1);
|
||||
if (!out) return NULL;
|
||||
memcpy(out, view, slen);
|
||||
out[slen] = '\0';
|
||||
return out;
|
||||
}
|
||||
#endif // NIM_FFI_CBOR_HELPERS
|
||||
|
||||
|
||||
// request map keys: {"config": TimerConfig}
|
||||
void *my_timer_create_cbor(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData);
|
||||
|
||||
// request map keys: {"req": EchoRequest}
|
||||
int my_timer_echo_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
// request: empty CBOR map (0xA0)
|
||||
int my_timer_version_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
// request map keys: {"req": ComplexRequest}
|
||||
int my_timer_complex_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
// request map keys: {"job": JobSpec, "retry": RetryPolicy, "schedule": ScheduleConfig}
|
||||
int my_timer_schedule_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
int my_timer_destroy(void *ctx);
|
||||
|
||||
uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);
|
||||
int my_timer_remove_event_listener(void *ctx, uint64_t listenerId);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_GEN_MY_TIMER_CBOR_H */
|
||||
11
ffi.nimble
11
ffi.nimble
@ -160,6 +160,17 @@ task genbindings_c, "Generate C bindings for the timer example":
|
||||
" -d:ffiSrcPath=../timer.nim" &
|
||||
" -o:/dev/null examples/timer/timer.nim"
|
||||
|
||||
task genbindings_swift, "Generate the Swift wrapper for the iOS timer example":
|
||||
# Emits Sources/MyTimer/MyTimer.swift over the native C ABI. The C headers it
|
||||
# imports (cheaders/) come from `nimble genbindings_c`; run that too if the
|
||||
# library's types or procs changed.
|
||||
exec "nim c " & nimFlagsOrc &
|
||||
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
|
||||
" -d:ffiGenBindings -d:targetLang=swift" &
|
||||
" -d:ffiOutputDir=examples/timer/ios/Sources/MyTimer" &
|
||||
" -d:ffiSrcPath=../timer.nim" &
|
||||
" -o:/dev/null examples/timer/timer.nim"
|
||||
|
||||
task genbindings_go, "Generate Go (cgo) bindings for the timer example":
|
||||
exec "nim c " & nimFlagsOrc &
|
||||
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
|
||||
|
||||
479
ffi/codegen/swift.nim
Normal file
479
ffi/codegen/swift.nim
Normal file
@ -0,0 +1,479 @@
|
||||
## Swift binding generator for the nim-ffi framework.
|
||||
##
|
||||
## Emits an idiomatic Swift wrapper (`<Lib>.swift`) over the *native*
|
||||
## (zero-serialization) C ABI declared in `<lib>.h` (produced by the C
|
||||
## generator, see `c.nim`). The Swift side imports the C header through a
|
||||
## `C<Lib>` clang module (see the package's module map) and never touches CBOR.
|
||||
##
|
||||
## Shape mirrors the proven hand-written iOS example: 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 `DispatchSemaphore`. The wrapper
|
||||
## blocks on the semaphore until the callback fires, so a by-value request struct
|
||||
## and any `strdup`'d C strings stay alive on the caller's stack for the whole
|
||||
## call.
|
||||
##
|
||||
## Result decoding follows the native ABI's three callback shapes, picked per
|
||||
## proc from its metadata:
|
||||
## - ctor / void proc -> only `ret` (+ raw error text on RET_ERR)
|
||||
## - `string` return -> `(msg, len)` is the raw UTF-8 string bytes
|
||||
## - struct return -> `msg` is a `const <Type>*`; fields are copied out
|
||||
## INSIDE the callback (valid only for its lifetime) into a native Swift
|
||||
## value.
|
||||
##
|
||||
## Scope (first increment): procs whose parameters are scalars / strings / bools
|
||||
## / floats, or a single `{.ffi.}` struct whose fields are all of those. Procs
|
||||
## that need `seq[T]` / `Option[T]` parameter marshaling (or more than one struct
|
||||
## parameter) are skipped with a logged notice — that marshaling is the next
|
||||
## increment, kept out so the generated wrapper always compiles.
|
||||
|
||||
import std/[os, strutils]
|
||||
import ./meta, ./string_helpers
|
||||
|
||||
proc swiftModuleName(libName: string): string =
|
||||
## The clang-module name the wrapper imports, e.g. "my_timer" -> "CMyTimer".
|
||||
## Must match the package's module map.
|
||||
return "C" & snakeToPascalCase(libName)
|
||||
|
||||
proc isSimpleScalar(typeName: string): bool =
|
||||
## A field/param type that crosses by value with no array/option marshaling.
|
||||
let t = typeName.strip()
|
||||
case t
|
||||
of "string", "cstring", "bool", "float", "float32", "float64", "int", "int64",
|
||||
"int32", "int16", "int8", "clong", "cint", "uint", "uint64", "uint32",
|
||||
"uint16", "uint8", "byte", "csize_t":
|
||||
return true
|
||||
else:
|
||||
return false
|
||||
|
||||
proc swiftType(typeName: string): string =
|
||||
## Maps a simple Nim type to the Swift type used in the public API surface.
|
||||
let t = typeName.strip()
|
||||
case t
|
||||
of "string", "cstring": "String"
|
||||
of "bool": "Bool"
|
||||
of "float", "float32": "Float"
|
||||
of "float64": "Double"
|
||||
of "uint", "uint64", "uint32", "uint16", "uint8", "byte", "csize_t": "UInt"
|
||||
else: "Int" # all the signed integer aliases
|
||||
|
||||
proc swiftDefault(typeName: string): string =
|
||||
## Default value so flattened numeric/bool params are optional at the call
|
||||
## site (matches the hand-written `delayMs: Int = 0`). Strings get no default.
|
||||
let t = typeName.strip()
|
||||
if t == "bool":
|
||||
return "false"
|
||||
if t in ["string", "cstring"]:
|
||||
return ""
|
||||
return "0"
|
||||
|
||||
type FieldPlan = object
|
||||
name: string
|
||||
typeName: string
|
||||
|
||||
proc structFields(types: seq[FFITypeMeta], typeName: string): seq[FieldPlan] =
|
||||
var plan: seq[FieldPlan] = @[]
|
||||
for t in types:
|
||||
if t.name == typeName:
|
||||
for f in t.fields:
|
||||
plan.add(FieldPlan(name: f.name, typeName: f.typeName))
|
||||
return plan
|
||||
|
||||
proc allFieldsSimple(types: seq[FFITypeMeta], typeName: string): bool =
|
||||
let fields = structFields(types, typeName)
|
||||
if fields.len == 0:
|
||||
return false # not a known {.ffi.} struct
|
||||
for f in fields:
|
||||
if not isSimpleScalar(f.typeName):
|
||||
return false
|
||||
return true
|
||||
|
||||
proc isKnownStruct(types: seq[FFITypeMeta], typeName: string): bool =
|
||||
for t in types:
|
||||
if t.name == typeName:
|
||||
return true
|
||||
return false
|
||||
|
||||
proc canEmit(p: FFIProcMeta, types: seq[FFITypeMeta]): bool =
|
||||
## True when the proc's parameters are entirely covered by this increment:
|
||||
## zero params, all-scalar params, or exactly one struct param of simple
|
||||
## fields. Multiple struct params or seq/Option fields are deferred.
|
||||
var structParams = 0
|
||||
for ep in p.extraParams:
|
||||
if isSimpleScalar(ep.typeName):
|
||||
continue
|
||||
if isKnownStruct(types, ep.typeName):
|
||||
inc structParams
|
||||
if not allFieldsSimple(types, ep.typeName):
|
||||
return false
|
||||
else:
|
||||
return false # ptr / unknown type
|
||||
return structParams <= 1
|
||||
|
||||
# --- Swift value emitted for a flattened parameter list -----------------------
|
||||
|
||||
proc flattenedParams(
|
||||
p: FFIProcMeta, types: seq[FFITypeMeta]
|
||||
): seq[FieldPlan] =
|
||||
## The Swift method's parameters: scalar params verbatim, plus the fields of
|
||||
## the single struct param flattened in (matching the example's
|
||||
## `echo(_ message:, delayMs:)`).
|
||||
var params: seq[FieldPlan] = @[]
|
||||
for ep in p.extraParams:
|
||||
if isSimpleScalar(ep.typeName):
|
||||
params.add(FieldPlan(name: ep.name, typeName: ep.typeName))
|
||||
else:
|
||||
params.add(structFields(types, ep.typeName))
|
||||
return params
|
||||
|
||||
proc swiftParamList(params: seq[FieldPlan], labelFirst: bool): string =
|
||||
## `labelFirst` keeps the first parameter labeled (ctors read `init(name:)`);
|
||||
## methods leave it unlabeled for an idiomatic `echo("msg")` call site.
|
||||
var parts: seq[string] = @[]
|
||||
for i, fp in params:
|
||||
let label = if i == 0 and not labelFirst: "_ " & fp.name else: fp.name
|
||||
var decl = label & ": " & swiftType(fp.typeName)
|
||||
if fp.typeName.strip() notin ["string", "cstring"]:
|
||||
decl &= " = " & swiftDefault(fp.typeName)
|
||||
parts.add(decl)
|
||||
return parts.join(", ")
|
||||
|
||||
# --- Marshaling a flattened param into its C representation -------------------
|
||||
|
||||
proc emitParamMarshal(
|
||||
lines: var seq[string], p: FFIProcMeta, types: seq[FFITypeMeta], modName: string
|
||||
): seq[string] =
|
||||
## Emits the C-side locals for the call and returns the list of C argument
|
||||
## expressions to pass after `(ctx, cb, ud, ...)`. String params are `strdup`'d
|
||||
## and freed via `defer`; struct params are built field by field.
|
||||
var cArgs: seq[string] = @[]
|
||||
for ep in p.extraParams:
|
||||
if isSimpleScalar(ep.typeName):
|
||||
if ep.typeName.strip() in ["string", "cstring"]:
|
||||
let cvar = "c_" & ep.name
|
||||
lines.add(" let " & cvar & " = strdup(" & ep.name & ")")
|
||||
lines.add(" defer { free(" & cvar & ") }")
|
||||
cArgs.add("UnsafePointer(" & cvar & ")")
|
||||
else:
|
||||
cArgs.add(swiftType(ep.typeName) & "(" & ep.name & ")")
|
||||
else:
|
||||
# single struct param: build it field by field from the flattened args
|
||||
let sv = "c_" & ep.name
|
||||
lines.add(" var " & sv & " = " & modName & "." & ep.typeName & "()")
|
||||
for f in structFields(types, ep.typeName):
|
||||
if f.typeName.strip() in ["string", "cstring"]:
|
||||
let cvar = "c_" & ep.name & "_" & f.name
|
||||
lines.add(" let " & cvar & " = strdup(" & f.name & ")")
|
||||
lines.add(" defer { free(" & cvar & ") }")
|
||||
lines.add(" " & sv & "." & f.name & " = UnsafePointer(" & cvar & ")")
|
||||
elif f.typeName.strip() == "bool":
|
||||
lines.add(" " & sv & "." & f.name & " = " & f.name & " ? 1 : 0")
|
||||
else:
|
||||
let ct =
|
||||
if f.typeName.strip() in [
|
||||
"uint", "uint64", "uint32", "uint16", "uint8", "byte", "csize_t"
|
||||
]: "UInt64"
|
||||
elif f.typeName.strip() in ["float", "float32"]: "Float"
|
||||
elif f.typeName.strip() == "float64": "Double"
|
||||
else: "Int64"
|
||||
lines.add(" " & sv & "." & f.name & " = " & ct & "(" & f.name & ")")
|
||||
cArgs.add(sv)
|
||||
return cArgs
|
||||
|
||||
# --- Result structs (one Swift value type per struct-returning proc) ----------
|
||||
|
||||
proc emitResultStruct(lines: var seq[string], typeName: string, fields: seq[FieldPlan]) =
|
||||
lines.add("public struct " & typeName & ": Equatable {")
|
||||
for f in fields:
|
||||
lines.add(" public let " & f.name & ": " & swiftType(f.typeName))
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# --- Per-proc method bodies ---------------------------------------------------
|
||||
|
||||
proc returnsString(p: FFIProcMeta): bool =
|
||||
return p.returnTypeName.strip() == "string"
|
||||
|
||||
proc returnsStruct(p: FFIProcMeta, types: seq[FFITypeMeta]): bool =
|
||||
return isKnownStruct(types, p.returnTypeName.strip())
|
||||
|
||||
proc boxName(p: FFIProcMeta): string =
|
||||
return snakeToPascalCase(p.procName) & "Box"
|
||||
|
||||
proc callbackName(p: FFIProcMeta): string =
|
||||
return p.procName & "Callback"
|
||||
|
||||
proc stripLibPrefix(procName, libName: string): string =
|
||||
## "my_timer_echo" -> "echo"; collapses the remaining snake_case to camelCase.
|
||||
var base = procName
|
||||
if base.startsWith(libName & "_"):
|
||||
base = base[libName.len + 1 .. ^1]
|
||||
let parts = base.split('_')
|
||||
var s = parts[0]
|
||||
for i in 1 ..< parts.len:
|
||||
s &= capitalizeFirstLetter(parts[i])
|
||||
return s
|
||||
|
||||
proc emitMethod(
|
||||
lines: var seq[string], p: FFIProcMeta, types: seq[FFITypeMeta], modName: string
|
||||
) =
|
||||
let params = flattenedParams(p, types)
|
||||
let paramList = swiftParamList(params, labelFirst = p.kind == FFIKind.CTOR)
|
||||
case p.kind
|
||||
of FFIKind.CTOR:
|
||||
lines.add(" public init(" & paramList & ") throws {")
|
||||
lines.add(" let box = Box()")
|
||||
lines.add(" let ud = Unmanaged.passUnretained(box).toOpaque()")
|
||||
var marshalLines: seq[string] = @[]
|
||||
let cArgs = emitParamMarshal(marshalLines, p, types, modName)
|
||||
lines.add(marshalLines)
|
||||
let lead = if cArgs.len > 0: cArgs.join(", ") & ", " else: ""
|
||||
lines.add(
|
||||
" guard let c = " & p.procName & "(" & lead & "ackCallback, ud) else {"
|
||||
)
|
||||
lines.add(" throw TimerError.failed(\"create returned null\")")
|
||||
lines.add(" }")
|
||||
lines.add(" box.sem.wait()")
|
||||
lines.add(" guard box.ret == 0 else { throw TimerError.failed(box.text) }")
|
||||
lines.add(" ctx = c")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
of FFIKind.DTOR:
|
||||
lines.add(" deinit { " & p.procName & "(ctx) }")
|
||||
lines.add("")
|
||||
of FFIKind.FFI:
|
||||
var marshalLines: seq[string] = @[]
|
||||
let cArgs = emitParamMarshal(marshalLines, p, types, modName)
|
||||
let tail = if cArgs.len > 0: ", " & cArgs.join(", ") else: ""
|
||||
let methodName = stripLibPrefix(p.procName, p.libName)
|
||||
if returnsStruct(p, types):
|
||||
let retFields = structFields(types, p.returnTypeName)
|
||||
lines.add(
|
||||
" public func " & methodName & "(" & paramList & ") throws -> " &
|
||||
p.returnTypeName & " {"
|
||||
)
|
||||
lines.add(" let box = " & boxName(p) & "()")
|
||||
lines.add(" let ud = Unmanaged.passUnretained(box).toOpaque()")
|
||||
lines.add(marshalLines)
|
||||
lines.add(
|
||||
" guard " & p.procName & "(ctx, " & callbackName(p) & ", ud" & tail &
|
||||
") == 0 else {"
|
||||
)
|
||||
lines.add(
|
||||
" throw TimerError.failed(\"" & methodName & " dispatch failed\")"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add(" box.sem.wait()")
|
||||
lines.add(" guard box.ret == 0 else { throw TimerError.failed(box.text) }")
|
||||
var ctorArgs: seq[string] = @[]
|
||||
for f in retFields:
|
||||
ctorArgs.add(f.name & ": box." & f.name)
|
||||
lines.add(" return " & p.returnTypeName & "(" & ctorArgs.join(", ") & ")")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
elif returnsString(p):
|
||||
lines.add(
|
||||
" public func " & methodName & "(" & paramList & ") throws -> String {"
|
||||
)
|
||||
lines.add(" let box = Box()")
|
||||
lines.add(" let ud = Unmanaged.passUnretained(box).toOpaque()")
|
||||
lines.add(marshalLines)
|
||||
lines.add(
|
||||
" guard " & p.procName & "(ctx, stringCallback, ud" & tail &
|
||||
") == 0 else {"
|
||||
)
|
||||
lines.add(
|
||||
" throw TimerError.failed(\"" & methodName & " dispatch failed\")"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add(" box.sem.wait()")
|
||||
lines.add(" guard box.ret == 0 else { throw TimerError.failed(box.text) }")
|
||||
lines.add(" return box.text")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
else:
|
||||
lines.add(" public func " & methodName & "(" & paramList & ") throws {")
|
||||
lines.add(" let box = Box()")
|
||||
lines.add(" let ud = Unmanaged.passUnretained(box).toOpaque()")
|
||||
lines.add(marshalLines)
|
||||
lines.add(
|
||||
" guard " & p.procName & "(ctx, ackCallback, ud" & tail &
|
||||
") == 0 else {"
|
||||
)
|
||||
lines.add(
|
||||
" throw TimerError.failed(\"" & methodName & " dispatch failed\")"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add(" box.sem.wait()")
|
||||
lines.add(" guard box.ret == 0 else { throw TimerError.failed(box.text) }")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# --- Per-struct-return callback + box -----------------------------------------
|
||||
|
||||
proc emitStructCallback(
|
||||
lines: var seq[string], p: FFIProcMeta, types: seq[FFITypeMeta], modName: string
|
||||
) =
|
||||
let retFields = structFields(types, p.returnTypeName)
|
||||
lines.add("final class " & boxName(p) & " {")
|
||||
lines.add(" var ret: Int32 = -1")
|
||||
lines.add(" var text = \"\"")
|
||||
for f in retFields:
|
||||
var init = ": " & swiftType(f.typeName) & " = 0"
|
||||
if f.typeName.strip() in ["string", "cstring"]:
|
||||
init = " = \"\""
|
||||
elif f.typeName.strip() == "bool":
|
||||
init = " = false"
|
||||
lines.add(" var " & f.name & init)
|
||||
lines.add(" let sem = DispatchSemaphore(value: 0)")
|
||||
lines.add("}")
|
||||
lines.add(
|
||||
"private func " & callbackName(p) &
|
||||
"(_ ret: Int32, _ msg: UnsafePointer<CChar>?,"
|
||||
)
|
||||
lines.add(" _ len: Int, _ ud: UnsafeMutableRawPointer?) {")
|
||||
lines.add(
|
||||
" let box = Unmanaged<" & boxName(p) & ">.fromOpaque(ud!).takeUnretainedValue()"
|
||||
)
|
||||
lines.add(" box.ret = ret")
|
||||
lines.add(" if ret == 0, let m = msg {")
|
||||
lines.add(
|
||||
" let resp = UnsafeRawPointer(m).assumingMemoryBound(to: " & modName & "." &
|
||||
p.returnTypeName & ".self)"
|
||||
)
|
||||
for f in retFields:
|
||||
if f.typeName.strip() in ["string", "cstring"]:
|
||||
lines.add(
|
||||
" box." & f.name & " = resp.pointee." & f.name &
|
||||
".map { String(cString: $0) } ?? \"\""
|
||||
)
|
||||
elif f.typeName.strip() == "bool":
|
||||
lines.add(" box." & f.name & " = resp.pointee." & f.name & " != 0")
|
||||
else:
|
||||
lines.add(
|
||||
" box." & f.name & " = " & swiftType(f.typeName) & "(resp.pointee." &
|
||||
f.name & ")"
|
||||
)
|
||||
lines.add(" } else {")
|
||||
lines.add(" box.text = rawText(msg, len)")
|
||||
lines.add(" }")
|
||||
lines.add(" box.sem.signal()")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# --- Static preamble (shared error type, Box, rawText, ack/string callbacks) --
|
||||
|
||||
proc preamble(modName: string): string =
|
||||
return (
|
||||
"""
|
||||
// Generated by nim-ffi Swift codegen. Do not edit by hand.
|
||||
//
|
||||
// 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 """ & modName & "\n" &
|
||||
"""import Foundation
|
||||
|
||||
public enum TimerError: Error, CustomStringConvertible {
|
||||
case failed(String)
|
||||
public var description: String {
|
||||
switch self { case let .failed(m): return m }
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
const SharedPlumbing =
|
||||
"""
|
||||
// MARK: - shared callback plumbing
|
||||
final class Box {
|
||||
var ret: Int32 = -1
|
||||
var text = ""
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
"""
|
||||
|
||||
proc generateSwiftWrapper*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
): string =
|
||||
let modName = swiftModuleName(libName)
|
||||
let className = snakeToPascalCase(libName) & "Node"
|
||||
var lines: seq[string] = @[]
|
||||
lines.add(preamble(modName))
|
||||
|
||||
# Emit a Swift result struct for every struct that appears as a return type.
|
||||
var emittedResults: seq[string] = @[]
|
||||
for p in procs:
|
||||
if not canEmit(p, types):
|
||||
continue
|
||||
let rt = p.returnTypeName.strip()
|
||||
if isKnownStruct(types, rt) and rt notin emittedResults:
|
||||
emittedResults.add(rt)
|
||||
emitResultStruct(lines, rt, structFields(types, rt))
|
||||
|
||||
# The wrapper class.
|
||||
lines.add("public final class " & className & " {")
|
||||
lines.add(" private let ctx: UnsafeMutableRawPointer")
|
||||
lines.add("")
|
||||
for p in procs:
|
||||
if not canEmit(p, types):
|
||||
continue
|
||||
emitMethod(lines, p, types, modName)
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
lines.add(SharedPlumbing)
|
||||
lines.add("")
|
||||
|
||||
# Per-struct-return callbacks + their boxes.
|
||||
for p in procs:
|
||||
if not canEmit(p, types):
|
||||
continue
|
||||
if returnsStruct(p, types):
|
||||
emitStructCallback(lines, p, types, modName)
|
||||
|
||||
return lines.join("\n")
|
||||
|
||||
proc generateSwiftBindings*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
) =
|
||||
# Report procs deferred to the seq/Option marshaling increment so coverage is
|
||||
# never silently dropped.
|
||||
for p in procs:
|
||||
if not canEmit(p, types):
|
||||
echo "swift codegen: skipping '" & p.procName &
|
||||
"' (needs seq/Option or multi-struct param marshaling — not yet supported)"
|
||||
writeFile(
|
||||
outputDir / (snakeToPascalCase(libName) & ".swift"),
|
||||
generateSwiftWrapper(procs, types, libName),
|
||||
)
|
||||
@ -9,6 +9,7 @@ when defined(ffiGenBindings):
|
||||
import ../codegen/cddl
|
||||
import ../codegen/c
|
||||
import ../codegen/go
|
||||
import ../codegen/swift
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# String helpers used by multiple macros
|
||||
@ -1988,10 +1989,15 @@ macro genBindings*(
|
||||
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath,
|
||||
ffiEventRegistry,
|
||||
)
|
||||
of "swift":
|
||||
generateSwiftBindings(
|
||||
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath,
|
||||
ffiEventRegistry,
|
||||
)
|
||||
else:
|
||||
error(
|
||||
"genBindings: unknown targetLang '" & lang &
|
||||
"'. Use 'c', 'go', 'rust', 'cpp', or 'cddl'."
|
||||
"'. Use 'c', 'go', 'swift', 'rust', 'cpp', or 'cddl'."
|
||||
)
|
||||
|
||||
return newEmptyNode()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user