diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md index 0cc4ceba..29d7df2e 100644 --- a/Plugins/BridgeJS/README.md +++ b/Plugins/BridgeJS/README.md @@ -4,7 +4,7 @@ > This feature is still experimental, and the API may change frequently. Use at your own risk with `JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1` environment variable. > [!NOTE] -> This documentation is intended for JavaScriptKit developers, not JavaScriptKit users. +> This documentation is intended for JavaScriptKit developers, not JavaScriptKit users. For user documentation, see [Exporting Swift to JavaScript](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-to-javascript) and [Importing TypeScript into Swift](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/importing-typescript-into-swift). ## Overview @@ -14,6 +14,7 @@ BridgeJS provides easy interoperability between Swift and JavaScript/TypeScript. 2. **Exporting Swift APIs to JavaScript**: Make your Swift APIs available to JavaScript code The workflow is: + 1. `ts2swift` converts TypeScript definitions (`bridge-js.d.ts`) to macro-annotated Swift declarations (`BridgeJS.Macros.swift`) 2. `bridge-js generate` processes both Swift source files (for export) and macro-annotated Swift files (for import) to generate: - `BridgeJS.swift` (Swift glue code) @@ -59,27 +60,53 @@ graph LR ## Type Mapping -### Primitive Type Conversions - -TBD - -| Swift Type | JS Type | Wasm Core Type | -|:--------------|:-----------|:---------------| -| `Int` | `number` | `i32` | -| `UInt` | `number` | `i32` | -| `Int8` | `number` | `i32` | -| `UInt8` | `number` | `i32` | -| `Int16` | `number` | `i32` | -| `UInt16` | `number` | `i32` | -| `Int32` | `number` | `i32` | -| `UInt32` | `number` | `i32` | -| `Int64` | `bigint` | `i64` | -| `UInt64` | `bigint` | `i64` | -| `Float` | `number` | `f32` | -| `Double` | `number` | `f64` | -| `Bool` | `boolean` | `i32` | -| `Void` | `void` | - | -| `String` | `string` | `i32` | +### Primitive Types + +| Swift Type | TypeScript Type | Wasm Core Type | +|:--------------|:----------------|:---------------| +| `Int` | `number` | `i32` | +| `UInt` | `number` | `i32` | +| `Int8` | `number` | `i32` | +| `UInt8` | `number` | `i32` | +| `Int16` | `number` | `i32` | +| `UInt16` | `number` | `i32` | +| `Int32` | `number` | `i32` | +| `UInt32` | `number` | `i32` | +| `Int64` | `bigint` | `i64` | +| `UInt64` | `bigint` | `i64` | +| `Float` | `number` | `f32` | +| `Double` | `number` | `f64` | +| `Bool` | `boolean` | `i32` | +| `Void` | `void` | - | + +### Complex Types + +| Swift Type | TypeScript Type | Semantics | Status | +|:-----------|:----------------|:----------|:-------| +| `String` | `string` | Copy | ✅ | +| `@JS class` | `interface` + constructor | Reference (pointer) | ✅ | +| `@JS struct` | `interface` | Copy (fields via stacks) | ✅ | +| `@JS enum` (case) | const object + tag type | Copy (integer) | ✅ | +| `@JS enum` (raw value) | const object + tag type | Copy (raw value) | ✅ | +| `@JS enum` (associated) | discriminated union | Copy (fields via stacks) | ✅ | +| `@JS protocol` | `interface` | Reference (wrapper) | ✅ | +| `Optional` | `T \| null` | Depends on T | ✅ | +| `(T) -> U` | `(arg: T) => U` | Reference (boxed) | ✅ | +| `JSObject` | `any` / `object` | Reference | ✅ | +| `Array` | `T[]` | Copy | ✅ | +| `Array>` | `T[][]` | Copy | ✅ | +| `Dictionary` | `Record` | - | [#495](https://github.com/swiftwasm/JavaScriptKit/issues/495) | +| `Set` | `Set` | - | [#397](https://github.com/swiftwasm/JavaScriptKit/issues/397) | +| `Foundation.URL` | `string` | - | [#496](https://github.com/swiftwasm/JavaScriptKit/issues/496) | +| Generics | - | - | [#398](https://github.com/swiftwasm/JavaScriptKit/issues/398) | + +### Import-specific (TypeScript → Swift) + +| TypeScript Type | Swift Type | Status | +|:----------------|:-----------|:-------| +| `T \| null` | `Optional` | [#475](https://github.com/swiftwasm/JavaScriptKit/issues/475) | +| `T \| undefined` | `Optional` | [#475](https://github.com/swiftwasm/JavaScriptKit/issues/475) | +| `enum` | `@JS enum` | [#489](https://github.com/swiftwasm/JavaScriptKit/issues/489) | ## Type Modeling @@ -109,23 +136,37 @@ The ABI will not be stable, and not meant to be interposed by other tools. ### Parameter Passing -Parameter passing follows Wasm calling conventions, with custom handling for complex types like strings and objects. +Parameter passing follows Wasm calling conventions, with custom handling for complex types: -TBD +- **Primitives**: Passed directly as Wasm arguments (`i32`, `i64`, `f32`, `f64`) +- **Strings**: UTF-8 bytes stored in `swift.memory`, ID + length passed as Wasm arguments +- **Swift Classes**: Raw Swift heap pointer passed as `i32` +- **JSObjects**: Object stored in `swift.memory.heap`, object ID passed as `i32` +- **Structs/Arrays**: Fields/elements pushed to type-specific stacks, Swift pops in reverse order +- **Closures**: Boxed and retained in memory, handle passed as `i32` ### Return Values -TBD +Return values use direct Wasm returns for primitives, and imported intrinsic functions for complex types: + +- **Primitives**: Returned directly via Wasm return value +- **Strings**: Swift writes UTF-8 bytes to shared memory, JS decodes +- **Swift Classes**: Pointer returned directly, JS wraps in `SwiftHeapObject` with `FinalizationRegistry` +- **Structs/Arrays**: Swift pushes fields/elements to type-specific stacks, JS reconstructs + +### Memory Management + +- **Swift Classes**: Live on Swift heap. JS holds pointer wrapped in `SwiftHeapObject`. `FinalizationRegistry` calls `deinit` on GC. Optional `release()` for deterministic cleanup. +- **JSObjects**: Live in `swift.memory.heap` (JS side). Swift holds ID wrapped in `JSObject`. Reference counted via `retain`/`release`. +- **Structs/Arrays/Enums**: Copy semantics - data serialized across boundary. No cleanup needed. +- **Closures**: Boxed on source side, released when GC'd on either side. + +For detailed semantics, see the [How It Works sections](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-class#How-It-Works) in the user documentation. ## Future Work -- [ ] Struct on parameter or return type -- [ ] Throws functions -- [ ] Async functions - [ ] Cast between TS interface -- [ ] Closure support -- [ ] Simplify constructor pattern - * https://github.com/ocsigen/ts2ocaml/blob/main/docs/js_of_ocaml.md#feature-immediate-constructor +- [ ] Simplify constructor pattern ([reference](https://github.com/ocsigen/ts2ocaml/blob/main/docs/js_of_ocaml.md#feature-immediate-constructor)) ```typescript interface Foo = { someMethod(value: number): void; @@ -141,3 +182,8 @@ TBD ``` - [ ] Use `externref` once it's widely available - [ ] Test SwiftObject roundtrip +- [ ] Import TS `enum` as Swift enum ([#489](https://github.com/swiftwasm/JavaScriptKit/issues/489)) +- [ ] Support `T | null` and `T | undefined` imports ([#475](https://github.com/swiftwasm/JavaScriptKit/issues/475)) +- [ ] Support `@JS var` for global scope imports ([#466](https://github.com/swiftwasm/JavaScriptKit/issues/466)) +- [ ] Support `export { thing } from 'pkg'` form ([#437](https://github.com/swiftwasm/JavaScriptKit/issues/437)) +- [ ] Support imported TS type usage on exported interface ([#497](https://github.com/swiftwasm/JavaScriptKit/issues/497)) diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md index 3a4df4ae..3e44581f 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md @@ -76,6 +76,17 @@ export type Exports = { } ``` +## How It Works + +Classes use **reference semantics** when crossing the Swift/JavaScript boundary: + +1. **Object Creation**: When you create a class instance (via `new` in JS), the object lives on the Swift heap +2. **Reference Passing**: JavaScript receives a reference (handle) to the Swift object, not a copy +3. **Shared State**: Changes made through either Swift or JavaScript affect the same object +4. **Memory Management**: `FinalizationRegistry` automatically releases Swift objects when they're garbage collected in JavaScript. You can optionally call `release()` for deterministic cleanup. + +This differs from structs, which use copy semantics and transfer data by value. + ## Supported Features | Swift Feature | Status | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md index 498e6c07..fe934ddb 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md @@ -1,4 +1,4 @@ -# Exporting Swift Closures +# Exporting Swift Closures to JS Learn how to use closure/function types as parameters and return values in BridgeJS. @@ -89,18 +89,20 @@ export type Exports = { } ``` -## Memory Management +## How It Works -When JavaScript passes a function to Swift, it's automatically stored in `swift.memory` with reference counting. When Swift's closure wrapper is deallocated by ARC, the JavaScript function is released. +Closures use **reference semantics** when crossing the Swift/JavaScript boundary: -When Swift returns a closure to JavaScript, the Swift closure is boxed and automatically released when the JavaScript function is garbage collected. +1. **JavaScript → Swift**: When JavaScript passes a function to Swift, it's stored in `swift.memory` with reference counting. When Swift's closure wrapper is deallocated by ARC, the JavaScript function is released. +2. **Swift → JavaScript**: When Swift returns a closure to JavaScript, the Swift closure is boxed and automatically released when the JavaScript function is garbage collected. +3. **Memory Management**: Both directions use automatic memory management via `FinalizationRegistry` - no manual cleanup required. -Both directions use automatic memory management - no manual cleanup required. +This differs from structs and arrays, which use copy semantics and transfer data by value. ## Supported Features -| Feature | Status | -|:--------|:-------| +| Swift Feature | Status | +|:--------------|:-------| | Closure Parameters with Supported Types `(String, Int) -> Person` | ✅ | | Closure Return Value with Supported Types `() -> Person` | ✅ | | `@escaping` closures | ✅ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md index 9eec622f..21925685 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md @@ -143,6 +143,7 @@ custom.release(); ``` **Requirements:** + - Constructor/initializer arguments must be literal values (`"text"`, `42`, `true`, `false`, `nil`) - Struct initializers must use labeled arguments (e.g., `Point(x: 1.0, y: 2.0)`) - Complex expressions, computed properties, or method calls are not supported diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md index 93c1fdd2..3dda2b9c 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md @@ -19,9 +19,7 @@ BridgeJS generates separate objects with descriptive naming for `.const` enums: - **`EnumNameTag`**: Represents the union type for enums - **`EnumNameObject`**: Object type for all const-style enums, contains static members for enums with methods/properties or references the values type for simple enums -#### Case Enums - -**Swift Definition:** +### Case Enums ```swift @JS enum Direction { @@ -45,7 +43,7 @@ BridgeJS generates separate objects with descriptive naming for `.const` enums: } ``` -**Generated TypeScript Declaration:** +Generated TypeScript declarations: ```typescript // Const object style (default) @@ -73,7 +71,7 @@ export const StatusValues: { export type StatusTag = typeof StatusValues[keyof typeof StatusValues]; ``` -**Usage in TypeScript:** +In TypeScript: ```typescript const direction: DirectionTag = DirectionValues.North; @@ -100,66 +98,9 @@ function handleDirection(direction: DirectionTag) { } ``` -BridgeJS also generates convenience initializers and computed properties for each case-style enum, allowing the rest of the Swift glue code to remain minimal and consistent. This avoids repetitive switch statements in every function that passes enum values between JavaScript and Swift. +### Raw Value Enums -```swift -extension Direction { - init?(bridgeJSRawValue: Int32) { - switch bridgeJSRawValue { - case 0: - self = .north - case 1: - self = .south - case 2: - self = .east - case 3: - self = .west - default: - return nil - } - } - - var bridgeJSRawValue: Int32 { - switch self { - case .north: - return 0 - case .south: - return 1 - case .east: - return 2 - case .west: - return 3 - } - } -} -... -@_expose(wasm, "bjs_setDirection") -@_cdecl("bjs_setDirection") -public func _bjs_setDirection(direction: Int32) -> Void { - #if arch(wasm32) - setDirection(_: Direction(bridgeJSRawValue: direction)!) - #else - fatalError("Only available on WebAssembly") - #endif -} - -@_expose(wasm, "bjs_getDirection") -@_cdecl("bjs_getDirection") -public func _bjs_getDirection() -> Int32 { - #if arch(wasm32) - let ret = getDirection() - return ret.bridgeJSRawValue - #else - fatalError("Only available on WebAssembly") - #endif -} -``` - -#### Raw Value Enums - -##### String Raw Values - -**Swift Definition:** +#### String Raw Values ```swift // Default const object style @@ -177,7 +118,7 @@ public func _bjs_getDirection() -> Int32 { } ``` -**Generated TypeScript Declaration:** +Generated TypeScript declarations: ```typescript // Const object style (default) @@ -196,7 +137,7 @@ export enum TSTheme { } ``` -**Usage in TypeScript:** +In TypeScript: ```typescript // Raw value enums work similarly to case enums @@ -209,9 +150,7 @@ const currentTheme: ThemeTag = exports.getTheme(); const status: HttpStatusTag = exports.processTheme(ThemeValues.Auto); ``` -##### Integer Raw Values - -**Swift Definition:** +#### Integer Raw Values ```swift // Default const object style @@ -237,7 +176,7 @@ const status: HttpStatusTag = exports.processTheme(ThemeValues.Auto); } ``` -**Generated TypeScript Declaration:** +Generated TypeScript declarations: ```typescript // Const object style (default) @@ -265,7 +204,7 @@ export const PriorityValues: { export type PriorityTag = typeof PriorityValues[keyof typeof PriorityValues]; ``` -**Usage in TypeScript:** +In TypeScript: ```typescript const status: HttpStatusTag = HttpStatusValues.Ok; @@ -280,9 +219,7 @@ const convertedPriority: PriorityTag = exports.convertPriority(HttpStatusValues. ### Namespace Enums -Namespace enums are empty enums (containing no cases) used for organizing related types and functions into hierarchical namespaces. - -**Swift Definition:** +Namespace enums are empty enums (containing no cases) used for organizing related types and functions into hierarchical namespaces. See for more details on namespace organization. ```swift @JS enum Utils { @@ -325,7 +262,7 @@ enum Internal { } ``` -**Generated TypeScript Declaration:** +Generated TypeScript declarations: ```typescript declare global { @@ -364,7 +301,7 @@ declare global { } ``` -**Usage in TypeScript:** +In TypeScript: ```typescript // Access nested classes through namespaces (no globalThis prefix needed) @@ -386,7 +323,7 @@ Things to remember when using enums for namespacing: 2. Top-level enums can use `@JS(namespace: "Custom.Path")` to place themselves in custom namespaces, which will be used as "base namespace" for all nested elements as well 3. Classes and enums nested within namespace enums **cannot** use `@JS(namespace:)` - this would create conflicting namespace declarations -**Invalid Usage:** +Invalid usage: ```swift @JS enum Utils { @@ -397,7 +334,7 @@ Things to remember when using enums for namespacing: } ``` -**Valid Usage:** +Valid usage: ```swift // Valid - top-level enum with explicit namespace @@ -409,12 +346,10 @@ enum Helper { } ``` -#### Associated Value Enums +### Associated Value Enums Associated value enums are supported and allow you to pass data along with each enum case. BridgeJS generates TypeScript discriminated union types. Associated values are encoded into a binary format for efficient transfer between JavaScript and WebAssembly -**Swift Definition:** - ```swift @JS enum APIResult { @@ -440,7 +375,7 @@ enum ComplexResult { @JS func getResult() -> APIResult ``` -**Generated TypeScript Declaration:** +Generated TypeScript declarations: ```typescript export const APIResultValues: { @@ -482,7 +417,7 @@ export type ComplexResultTag = { tag: typeof ComplexResultValues.Tag.Info } ``` -**Usage in TypeScript:** +In TypeScript: ```typescript const successResult: APIResultTag = { @@ -528,21 +463,54 @@ function processResult(result: APIResultTag) { } ``` -**Supported Features:** +## How It Works + +Enums use **copy semantics** when crossing the Swift/JavaScript boundary: + +1. **Case Enums**: Passed as integer tag values (0, 1, 2, ...) representing each case +2. **Raw Value Enums**: Passed using their raw value type (string or integer) +3. **Associated Value Enums**: Tag value passed directly, associated data pushed to type-specific stacks and reconstructed on the receiving side + +BridgeJS generates convenience initializers and computed properties for each enum, keeping the glue code minimal and consistent: + +```swift +extension Direction { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: self = .north + case 1: self = .south + // ... + default: return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .north: return 0 + case .south: return 1 + // ... + } + } +} +``` + +This differs from classes, which use reference semantics and share state across the boundary. + +## Supported Features | Swift Feature | Status | |:--------------|:-------| -| Associated values: `String` | ✅ | -| Associated values: `Int` | ✅ | -| Associated values: `Bool` | ✅ | -| Associated values: `Float` | ✅ | -| Associated values: `Double` | ✅ | +| Case enums | ✅ | +| Raw value enums (`String`) | ✅ | +| Raw value enums (`Int`, `Int32`, etc.) | ✅ | +| Namespace enums (empty enums) | ✅ | +| Associated value enums | ✅ | +| `.tsEnum` style option | ✅ | +| Static functions | ✅ | +| Static properties | ✅ | +| Associated values: `String`, `Int`, `Bool`, `Float`, `Double` | ✅ | | Associated values: Custom classes/structs | ❌ | | Associated values: Other enums | ❌ | | Associated values: Arrays/Collections | ❌ | | Associated values: Optionals | ❌ | -| Use as exported function parameters | ✅ | -| Use as exported function return values | ✅ | -| Use as imported function parameters | ❌ | -| Use as imported function return values | ❌ | -| Namespace support | ✅ | +| Generics | ❌ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md index 24b04d74..91fdc347 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md @@ -112,8 +112,8 @@ export type UserProfile = { ## Supported Features -| Swift Optional Feature | Status | -|:----------------------|:-------| +| Swift Feature | Status | +|:--------------|:-------| | Optional primitive parameters: `Int?`, `String?`, etc. | ✅ | | Optional primitive return values | ✅ | | Optional object parameters: `MyClass?`, `JSObject?` | ✅ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md index 4dba78c3..111b1f05 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md @@ -1,4 +1,4 @@ -# Exporting Swift Protocols +# Exporting Swift Protocols to JS Learn how to expose Swift protocols to JavaScript as TypeScript interfaces. @@ -114,63 +114,6 @@ export type Exports = { } ``` -## Generated Wrapper - -BridgeJS generates a Swift wrapper struct for each `@JS` protocol. This wrapper holds a `JSObject` reference and forwards protocol method calls to the JavaScript implementation: - -```swift -struct AnyCounter: Counter, _BridgedSwiftProtocolWrapper { - let jsObject: JSObject - - var count: Int { - get { - @_extern(wasm, module: "TestModule", name: "bjs_Counter_count_get") - func _extern_get(this: Int32) -> Int32 - let ret = _extern_get(this: Int32(bitPattern: jsObject.id)) - return Int.bridgeJSLiftReturn(ret) - } - set { - @_extern(wasm, module: "TestModule", name: "bjs_Counter_count_set") - func _extern_set(this: Int32, value: Int32) - _extern_set(this: Int32(bitPattern: jsObject.id), value: newValue.bridgeJSLowerParameter()) - } - } - - var name: String { - @_extern(wasm, module: "TestModule", name: "bjs_Counter_name_get") - func _extern_get(this: Int32) - _extern_get(this: Int32(bitPattern: jsObject.id)) - return String.bridgeJSLiftReturn() - } - - func increment(by amount: Int) { - @_extern(wasm, module: "TestModule", name: "bjs_Counter_increment") - func _extern_increment(this: Int32, amount: Int32) - _extern_increment( - this: Int32(bitPattern: jsObject.id), - amount: amount.bridgeJSLowerParameter() - ) - } - - func reset() { - @_extern(wasm, module: "TestModule", name: "bjs_Counter_reset") - func _extern_reset(this: Int32) - _extern_reset(this: Int32(bitPattern: jsObject.id)) - } - - func getValue() -> Int { - @_extern(wasm, module: "TestModule", name: "bjs_Counter_getValue") - func _extern_getValue(this: Int32) -> Int32 - let ret = _extern_getValue(this: Int32(bitPattern: jsObject.id)) - return Int.bridgeJSLiftReturn(ret) - } - - static func bridgeJSLiftParameter(_ value: Int32) -> Self { - return AnyCounter(jsObject: JSObject(id: UInt32(bitPattern: value))) - } -} -``` - ## Swift Implementation You can also implement protocols in Swift and use them from JavaScript: @@ -223,12 +166,47 @@ console.log(counter.getValue()); // 0 ## How It Works -When you pass a JavaScript object implementing a protocol to Swift: +Protocols use **reference semantics** when crossing the Swift/JavaScript boundary: + +1. **JavaScript → Swift**: The JavaScript object is stored in JavaScriptKit's memory heap and its ID is passed as an `Int32` to Swift +2. **Wrapper Generation**: BridgeJS generates an `Any{ProtocolName}` wrapper struct that holds a `JSObject` reference and forwards protocol method calls through WASM to the JavaScript implementation +3. **Swift → JavaScript**: When returning a Swift protocol implementation to JavaScript, the object is stored on the Swift heap and JavaScript receives a reference +4. **Memory Management**: `FinalizationRegistry` automatically handles cleanup. The `JSObject` reference keeps the JavaScript object alive, and when the Swift wrapper is deallocated, the JavaScript object is released. + +### Generated Wrapper + +BridgeJS generates a Swift wrapper struct for each `@JS` protocol: + +```swift +struct AnyCounter: Counter, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject -1. **JavaScript Side**: The object is stored in JavaScriptKit's memory heap and its ID is passed as an `Int32` to Swift -2. **Swift Side**: BridgeJS creates an `Any{ProtocolName}` wrapper that holds a `JSObject` reference -3. **Method Calls**: Protocol method calls are forwarded through WASM to the JavaScript implementation -4. **Memory Management**: The `JSObject` reference keeps the JavaScript object alive using JavaScriptKit's retain/release system. When the Swift wrapper is deallocated, the JavaScript object is automatically released. + var count: Int { + get { + @_extern(wasm, module: "TestModule", name: "bjs_Counter_count_get") + func _extern_get(this: Int32) -> Int32 + let ret = _extern_get(this: Int32(bitPattern: jsObject.id)) + return Int.bridgeJSLiftReturn(ret) + } + set { + @_extern(wasm, module: "TestModule", name: "bjs_Counter_count_set") + func _extern_set(this: Int32, value: Int32) + _extern_set(this: Int32(bitPattern: jsObject.id), value: newValue.bridgeJSLowerParameter()) + } + } + + func increment(by amount: Int) { + @_extern(wasm, module: "TestModule", name: "bjs_Counter_increment") + func _extern_increment(this: Int32, amount: Int32) + _extern_increment( + this: Int32(bitPattern: jsObject.id), + amount: amount.bridgeJSLowerParameter() + ) + } + + // ... other protocol requirements +} +``` ## Supported Features diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md index 8602d937..00e7c1f2 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md @@ -122,7 +122,7 @@ const result: number = exports.Calculator.square(5); // 25 ## Namespace Enum Static Functions -Namespace enums organize related utility functions and are assigned to `globalThis`: +Namespace enums organize related utility functions and are assigned to `globalThis`. See for more details on namespace organization. ```swift @JS enum Utils { @@ -156,8 +156,8 @@ const result: string = Utils.String.uppercase("world"); ## Supported Features -| Swift Static Function Feature | Status | -|:------------------------------|:-------| +| Swift Feature | Status | +|:--------------|:-------| | Class `static func` | ✅ | | Class `class func` | ✅ | | Enum `static func` | ✅ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md index 097f644b..4110e616 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md @@ -128,7 +128,7 @@ exports.PropertyEnum.enumProperty = "updated"; ## Namespace Enum Static Properties -Namespace enums organize related static properties and are assigned to `globalThis`: +Namespace enums organize related static properties and are assigned to `globalThis`. See for more details on namespace organization. ```swift @JS enum PropertyNamespace { @@ -173,8 +173,8 @@ const value: number = PropertyNamespace.Nested.nestedProperty; ## Supported Features -| Swift Static Property Feature | Status | -|:------------------------------|:-------| +| Swift Feature | Status | +|:--------------|:-------| | Class `static let` | ✅ | | Class `static var` | ✅ | | Class `class var` | ✅ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md index e20a324c..236b822a 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md @@ -104,33 +104,9 @@ exports.Config.defaultTimeout = 60.0; console.log(exports.Config.maxRetries); // 3 (readonly) ``` -## Struct Methods +## Struct Methods and Initializers -Structs can have instance and static methods, both require @JS annotation: - -```swift -@JS struct Calculator { - @JS func add(a: Double, b: Double) -> Double { - return a + b - } - - @JS static func multiply(x: Double, y: Double) -> Double { - return x * y - } -} -``` - -In JavaScript: - -```javascript -const calc = {}; -console.log(exports.useMathOperations(calc, 5.0, 3.0)); // Uses instance methods -console.log(exports.Calculator.multiply(4.0, 5.0)); // Static method -``` - -## Struct Initializers - -Struct initializers are exported as static `init` methods, not constructors: +Structs can have instance methods, static methods, and initializers. All require `@JS` annotation: ```swift @JS struct Point { @@ -141,30 +117,46 @@ Struct initializers are exported as static `init` methods, not constructors: self.x = x self.y = y } + + @JS func distanceFromOrigin() -> Double { + return (x * x + y * y).squareRoot() + } + + @JS static func origin() -> Point { + return Point(x: 0, y: 0) + } } ``` In JavaScript: ```javascript -const point = exports.Point.init(10.0, 20.0); -console.log(point.x); // 10.0 +// Create via static init method (not constructor) +const point = exports.Point.init(3.0, 4.0); +console.log(point.distanceFromOrigin()); // 5.0 + +// Static method +const origin = exports.Point.origin(); +console.log(origin.x); // 0.0 ``` -This differs from classes, where `@JS init` maps to a JavaScript constructor using `new`: +Note: Struct initializers are exported as static `init` methods. This differs from classes, where `@JS init` maps to a JavaScript constructor using `new`. -```javascript -// Class: uses `new` -const cart = new exports.ShoppingCart(); +## How It Works -// Struct: uses static `init` method -const point = exports.Point.init(10.0, 20.0); -``` +Structs use **copy semantics** when crossing the Swift/JavaScript boundary: + +1. **Data Transfer**: Struct fields are pushed to type-specific stacks and reconstructed as plain JavaScript objects +2. **No Shared State**: Each side has its own copy - modifications don't affect the other +3. **No Memory Management**: No `release()` needed since there's no shared reference +4. **Plain Objects**: In JavaScript, structs become plain objects matching the TypeScript interface + +This differs from classes, which use reference semantics and share state across the boundary. ## Supported Features -| Feature | Status | -|:--------|:-------| +| Swift Feature | Status | +|:--------------|:-------| | Stored fields with supported types | ✅ | | Optional fields | ✅ | | Nested structs | ✅ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md index 0c4027b7..057b5f03 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md @@ -8,7 +8,11 @@ Learn how to organize exported Swift code into JavaScript namespaces. The `@JS` macro supports organizing your exported Swift code into namespaces using dot-separated strings. This allows you to create hierarchical structures in JavaScript that mirror your Swift code organization. -### Functions with Namespaces +There are two ways to create namespaces: +1. **`@JS(namespace:)`** - Explicitly specify a namespace for functions, classes, or enums +2. **Namespace enums** - Use empty enums to create implicit namespace hierarchies (see ) + +## Functions with Namespaces You can export functions to specific namespaces by providing a namespace parameter: @@ -49,7 +53,7 @@ declare global { } ``` -### Classes with Namespaces +## Classes with Namespaces For classes, you only need to specify the namespace on the top-level class declaration. All exported methods within the class will be part of that namespace: @@ -117,3 +121,7 @@ export interface Greeter extends SwiftHeapObject { ``` Using namespaces can be preferable for projects with many global functions, as they help prevent naming collisions. Namespaces also provide intuitive hierarchies for organizing your exported Swift code, and they do not affect the code generated by `@JS` declarations without namespaces. + +## See Also + +- - Namespace enums section