ReviewOS

also looking at this

craft-native/craft

iOS: hand unserved actions to the host shim, over one reply path

#84
Merged glennmichael123 wants to merge feat/ios-handoff into main
4 files +282 -24

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/ios/fixtures/zig-slice/build-and-run.shmodified+19-3
Changes to packages/ios/fixtures/zig-slice/build-and-run.sh
@@ -23,10 +23,26 @@ SDK="$(xcrun --sdk iphonesimulator --show-sdk-path)"
2323APP="$OUT/CraftSlice.app"
2424rm -rf "$OUT"; mkdir -p "$APP"
2525
26echo "==> compiling the fixture"
27# The ObjC translation units first, then swiftc drives the link. swiftc is the
28# linker rather than clang because Swift auto-links compatibility shims
29# (swiftCompatibility56 and friends) that live in the toolchain, not the SDK —
30# clang cannot find them and the link fails on undefined symbols.
31clang -isysroot "$SDK" -target "$TRIPLE" -fobjc-arc -O1 -c \
32 "$FIXTURE/main.m" -o "$OUT/main.o"
33clang -isysroot "$SDK" -target "$TRIPLE" -fobjc-arc -O1 -c \
34 "$FIXTURE/page.m" -o "$OUT/page.o"
35
2636echo "==> linking CraftSlice"
27clang -isysroot "$SDK" -target "$TRIPLE" \
28 -fobjc-arc -O1 \
29 "$FIXTURE/main.m" "$FIXTURE/page.m" "$FIXTURE/shim.m" \
37# The shim class has to survive into the binary for
38# `objc_getClass("CraftSwiftShim")` to find it at runtime. Compiling it into
39# the executable directly (rather than through an archive) is what guarantees
40# that: a linker is free to drop an archive member nothing references
41# statically, and a class reached only by name is exactly that.
42swiftc -target "$TRIPLE" -sdk "$SDK" \
43 -parse-as-library -O \
44 "$FIXTURE/shim.swift" \
45 "$OUT/main.o" "$OUT/page.o" \
3046 "$ROOT/packages/zig/zig-out/lib/$LIB" \
3147 -framework UIKit -framework WebKit -framework Foundation \
3248 -o "$APP/CraftSlice"
packages/ios/fixtures/zig-slice/page.mmodified+7-4
Changes to packages/ios/fixtures/zig-slice/page.m
@@ -42,10 +42,13 @@ const char craft_slice_page[] =
4242 " bridge.postMessage({t:'mobile', a:'getDeviceInfo', i:2});\n"
4343 " }\n"
4444 "\n"
45 " // Only the shim writes servedBy. Zig routed the call to it, the shim\n"
46 " // answered through craft_ios_deliver_result, and Zig delivered it here\n"
47 " // over the same protocol it uses for its own actions.\n"
48 " if (!sentHandOff && payload && payload.servedBy === 'host-shim') {\n"
45 " // `language` is written by the Swift shim and by nothing else, so this\n"
46 " // distinguishes a real hand-off from Zig answering by some other route.\n"
47 " // Zig routed the call out, Swift answered through\n"
48 " // craft_ios_deliver_result, and Zig delivered it here over the same\n"
49 " // protocol it uses for its own actions.\n"
50 " if (!sentHandOff && payload && payload.servedBy === 'host-shim'\n"
51 " && payload.language === 'swift') {\n"
4952 " sentHandOff = true;\n"
5053 " bridge.postMessage({t:'mobile', a:'getDeviceInfo', i:5});\n"
5154 " }\n"
packages/ios/fixtures/zig-slice/shim.mdeleted+0-46
Changes to packages/ios/fixtures/zig-slice/shim.m
@@ -1,46 +0,0 @@
1// A stand-in for the host shim, so the hand-off path can be exercised.
2//
3// Written in Objective-C rather than Swift on purpose. The contract Zig
4// depends on is a runtime one — `objc_getClass("CraftSwiftShim")` plus a
5// message send — and a `@objc` Swift class presents exactly that same
6// contract. Using ObjC here keeps the harness to a single clang invocation
7// with no swiftc, while testing the identical mechanism.
8//
9// The shape being proven is the important part: the shim answers *what*, never
10// *how*. It produces a payload and hands it back through
11// `craft_ios_deliver_result`, so the wire format, the request id and the
12// escaping all stay on the Zig side. Two components replying to one page by
13// two different routes is how this codebase accumulated five envelopes.
14#import <Foundation/Foundation.h>
15
16extern void craft_ios_deliver_result(const char *action, unsigned long action_len,
17 const char *json, unsigned long json_len,
18 long long request_id);
19
20@interface CraftSwiftShim : NSObject
21@end
22
23@implementation CraftSwiftShim
24
25+ (BOOL)handleAction:(NSString *)action
26 payload:(NSString *)payload
27 requestId:(long long)requestId {
28 // Exactly one action, to prove the seam. A real shim dispatches the
29 // ~105 the Swift template still owns.
30 if (![action isEqualToString:@"hostOnlyPing"]) {
31 return NO; // Not ours. Zig answers with UnknownAction.
32 }
33
34 NSString *json = [NSString stringWithFormat:
35 @"{\"servedBy\":\"host-shim\",\"echo\":%@}",
36 payload.length ? [NSString stringWithFormat:@"\"%lu\"", (unsigned long)payload.length] : @"null"];
37
38 const char *actionBytes = action.UTF8String;
39 const char *jsonBytes = json.UTF8String;
40 craft_ios_deliver_result(actionBytes, strlen(actionBytes),
41 jsonBytes, strlen(jsonBytes),
42 requestId);
43 return YES;
44}
45
46@end
packages/ios/fixtures/zig-slice/shim.swiftadded+57-0
Changes to packages/ios/fixtures/zig-slice/shim.swift
@@ -0,0 +1,57 @@
1// The host shim, in Swift, presenting the contract `CraftApp.swift` will.
2//
3// This replaces an Objective-C stand-in. The stand-in proved the runtime
4// mechanism — `objc_getClass` plus a message send — but not that *Swift*
5// presents that mechanism the way Zig expects: that `@objc(CraftSwiftShim)`
6// really does register under that name, that a `static func` becomes a class
7// method reachable by selector, that `String` parameters bridge from NSString,
8// and that `Bool` comes back as the ObjC BOOL Zig reads.
9//
10// Those are exactly the assumptions the real wiring will rest on, so they are
11// worth testing against the real compiler rather than assumed from a language
12// that shares an ABI with it.
13import Foundation
14
15// Zig owns the reply path. The shim decides *what* the answer is and hands it
16// back; the wire format, the request id and the escaping stay on the other
17// side. `@_silgen_name` reaches the exported symbol without needing a bridging
18// header, which keeps the harness to one swiftc invocation.
19@_silgen_name("craft_ios_deliver_result")
20func craft_ios_deliver_result(
21 _ action: UnsafePointer<CChar>, _ actionLen: UInt,
22 _ json: UnsafePointer<CChar>, _ jsonLen: UInt,
23 _ requestId: Int64
24)
25
26@objc(CraftSwiftShim)
27public class CraftSwiftShim: NSObject {
28 // Selector: handleAction:payload:requestId: — which is what
29 // `ios_dispatch.handOffToHost` sends. A mismatch here is not a compile
30 // error on either side, so the simulator assertion is what catches it.
31 @objc public static func handleAction(
32 _ action: String,
33 payload: String,
34 requestId: Int64
35 ) -> Bool {
36 // Exactly one action, to prove the seam. A real shim dispatches the
37 // ~105 the Swift template still owns.
38 guard action == "hostOnlyPing" else {
39 // Not ours. Zig answers UnknownAction, which is the same path a
40 // fully-migrated app with no shim at all takes.
41 return false
42 }
43
44 let json = "{\"servedBy\":\"host-shim\",\"language\":\"swift\",\"payloadBytes\":\(payload.utf8.count)}"
45
46 action.withCString { a in
47 json.withCString { j in
48 craft_ios_deliver_result(
49 a, UInt(strlen(a)),
50 j, UInt(strlen(j)),
51 requestId
52 )
53 }
54 }
55 return true
56 }
57}