also looking at this
iOS: hand unserved actions to the host shim, over one reply path
#84
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.
| @@ -23,10 +23,26 @@ SDK="$(xcrun --sdk iphonesimulator --show-sdk-path)" | ||
| 23 | 23 | APP="$OUT/CraftSlice.app" |
| 24 | 24 | rm -rf "$OUT"; mkdir -p "$APP" |
| 25 | 25 | |
| 26 | echo "==> 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. | |
| 31 | clang -isysroot "$SDK" -target "$TRIPLE" -fobjc-arc -O1 -c \ | |
| 32 | "$FIXTURE/main.m" -o "$OUT/main.o" | |
| 33 | clang -isysroot "$SDK" -target "$TRIPLE" -fobjc-arc -O1 -c \ | |
| 34 | "$FIXTURE/page.m" -o "$OUT/page.o" | |
| 35 | ||
| 26 | 36 | echo "==> linking CraftSlice" |
| 27 | clang -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. | |
| 42 | swiftc -target "$TRIPLE" -sdk "$SDK" \ | |
| 43 | -parse-as-library -O \ | |
| 44 | "$FIXTURE/shim.swift" \ | |
| 45 | "$OUT/main.o" "$OUT/page.o" \ | |
| 30 | 46 | "$ROOT/packages/zig/zig-out/lib/$LIB" \ |
| 31 | 47 | -framework UIKit -framework WebKit -framework Foundation \ |
| 32 | 48 | -o "$APP/CraftSlice" |
| @@ -42,10 +42,13 @@ const char craft_slice_page[] = | ||
| 42 | 42 | " bridge.postMessage({t:'mobile', a:'getDeviceInfo', i:2});\n" |
| 43 | 43 | " }\n" |
| 44 | 44 | "\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" | |
| 49 | 52 | " sentHandOff = true;\n" |
| 50 | 53 | " bridge.postMessage({t:'mobile', a:'getDeviceInfo', i:5});\n" |
| 51 | 54 | " }\n" |
| @@ -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 | ||
| 16 | extern 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 | |
| @@ -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. | |
| 13 | import 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") | |
| 20 | func craft_ios_deliver_result( | |
| 21 | _ action: UnsafePointer<CChar>, _ actionLen: UInt, | |
| 22 | _ json: UnsafePointer<CChar>, _ jsonLen: UInt, | |
| 23 | _ requestId: Int64 | |
| 24 | ) | |
| 25 | ||
| 26 | @objc(CraftSwiftShim) | |
| 27 | public 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 | } | |