概要
公開日: 2026-06-04
翻訳生成に失敗したため、原文をそのまま保存しています。
原文
TL;DR In SDK 56 we rewrote the native module infrastructure on Apple platforms so Swift talks to JSI directly. The Objective-C++ layer that used to sit in the call path is gone. Calls are faster, the stack is simpler, and a few things that were previously awkward become straightforward. This post is about the Apple side. The Android changes in SDK 56 (a Kotlin compiler plugin) get their own writeup. The old stack and why it had to change Before SDK 56, a JavaScript call into an Expo native module on Apple platforms went through three languages. Swift module code sat on top of a layer of Objective-C++ bindings (EXJavaScriptRuntime, EXJavaScriptValue, EXJavaScriptObject, and friends), which sat on top of JSI in C++. The Objective-C++ layer was there for a single reason: Swift couldn't call C++ directly, and Objective-C++ was the only practical glue. The cost showed up everywhere. Each call paid two language boundaries on the way in and two more on the way out, and every value got reshaped twice in each direction: std::string ↔ NSString ↔ Swift String, std::vector ↔ NSArray ↔ Swift Array, and so on. Each hop allocated and copied. Three languages had to be maintained in the hot path, and three languages also had to be reasoned about whenever something went wrong. Stack traces, types, and ownership all changed shape mid-call, which made debugging across the seam painful. It set a ceiling on how fast and how clean the API could be, and that ceiling is what the rewrite is about. Enter Swift/C++ interop Until recently, mixing Swift and C++ in the same project meant going through a mix of Objective-C and C++, usually called Objective-C++. Swift could talk to Objective-C, and Objective-C++ files could freely interleave Objective-C and C++. So any C++ type you wanted in Swift had to be wrapped in an Objective-C class first. The old stack was doing this on every call. Swift/C++ interop, introduced in Swift 5.9, removes the middle hop. Swift can import C++ headers directly. The compiler maps C++ types onto Swift types: classes and structs become Swift types you can construct, methods become Swift methods. No Objective-C class in between, no NSString/NSArray reshape on the way through. Coverage isn't total. Templates, for one, don't come through as Swift generics. But the bulk of an idiomatic C++ API surface does, and C++ types arrive with their ownership model intact. Combined with ~Copyable for the move-only ones, that's enough to preserve JSI's single-owner discipline up to the Swift API. The payoff is that a JSI call goes from a three-language relay race to a single Swift expression that lowers to a direct C++ call. Per-call cost is what you'd get from writing it in C++ to begin with. The cost moves elsewhere instead: compile times, plus some sharp edges around the boundary that we'll cover below. We're not the first to try this in the React Native space. Nitro Modules got there earlier, at a time when Swift/C++ interop was even less mature than it is now. Designing ExpoModulesJSI ExpoModulesJSI is the Swift package that wraps JSI in idiomatic Swift types. Despite the name, it knows nothing about Expo Modules; it's purely a Swift wrapper over JSI, and in principle we could ship it standalone. We don't, because it depends on React Native (which is where JSI lives) and JSI has essentially no users outside React Native. So the name is conservative on purpose. The core type system mirrors JSI one-to-one: JavaScriptRuntime, JavaScriptValue, JavaScriptObject, JavaScriptArray, JavaScriptFunction, JavaScriptArrayBuffer, JavaScriptTypedArray, JavaScriptBigInt, JavaScriptNativeState. Each maps to a JSI type, but exposed as a modern, type-safe Swift API. We mirror JSI's ownership semantics with non-copyable types. Many of JSI's value types are movable and non-copyable by design in C++. Take jsi::Value or jsi::Object: they own runtime resources, so the ownership is clear. At any point, exactly one place holds the value, and you have to be explicit