Skip to content

Commit 9146495

Browse files
BridgeJS: Update documentation for MVP release
1 parent e9c1529 commit 9146495

33 files changed

+1021
-510
lines changed

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Ahead-of-Time-Code-Generation.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Learn how to improve build times by generating BridgeJS code ahead of time.
66

77
> Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases.
88
9-
The BridgeJS build plugin automatically processes `@JS` annotations and TypeScript definitions during each build. While convenient, this can significantly increase build times for larger projects. To address this, JavaScriptKit provides a command plugin that lets you generate the bridge code ahead of time.
9+
The BridgeJS build plugin automatically processes macro annotations and TypeScript definitions during each build. While convenient, this can significantly increase build times for larger projects. To address this, JavaScriptKit provides a command plugin that lets you generate the bridge code ahead of time.
1010

1111
## Using the Command Plugin
1212

@@ -54,7 +54,7 @@ $ echo "{}" > Sources/MyApp/bridge-js.config.json
5454

5555
### Step 3: Create Your Swift Code with @JS Annotations
5656

57-
Write your Swift code with `@JS` annotations as usual:
57+
Write your Swift code with macro annotations as usual:
5858

5959
```swift
6060
import JavaScriptKit
@@ -65,13 +65,13 @@ import JavaScriptKit
6565

6666
@JS class Counter {
6767
private var count = 0
68-
68+
6969
@JS init() {}
70-
70+
7171
@JS func increment() {
7272
count += 1
7373
}
74-
74+
7575
@JS func getValue() -> Int {
7676
return count
7777
}
@@ -104,14 +104,15 @@ swift package plugin bridge-js
104104

105105
This command will:
106106

107-
1. Process all Swift files with `@JS` annotations
107+
1. Process all Swift files with macro annotations
108108
2. Process any TypeScript definition files
109109
3. Generate Swift binding code in a `Generated` directory within your source folder
110110

111111
For example, with a target named "MyApp", it will create:
112112

113113
```
114-
Sources/MyApp/Generated/BridgeJS.swift # Generated code for both exports and imports
114+
Sources/MyApp/Generated/BridgeJS.swift # Glue code for both exports and imports
115+
Sources/MyApp/Generated/BridgeJS.Macros.swift # Bridging interface generated from bridge-js.d.ts
115116
Sources/MyApp/Generated/JavaScript/BridgeJS.json # Unified skeleton JSON
116117
```
117118

@@ -173,4 +174,4 @@ git commit -m "Update generated BridgeJS code"
173174
2. **Version Control**: Always commit the generated files if using the command plugin
174175
3. **API Boundaries**: Try to stabilize your API boundaries to minimize regeneration
175176
4. **Documentation**: Document your approach in your project README
176-
5. **CI/CD**: If using the command plugin, consider verifying that generated code is up-to-date in CI
177+
5. **CI/CD**: If using the command plugin, consider verifying that generated code is up-to-date in CI

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Later files override settings from earlier files. This allows teams to commit a
4242

4343
Controls whether exported Swift APIs are exposed to the JavaScript global namespace (`globalThis`).
4444

45-
When `true`, exported functions, classes, and namespaces are available via `globalThis` in JavaScript. When `false`, they are only available through the exports object returned by `createExports()`.
45+
When `true`, exported functions, classes, and namespaces are available via `globalThis` in JavaScript. When `false`, they are only available through the exports object returned by `createExports()`.
4646
Using Exports provides better module isolation and support multiple WebAssembly instances in the same JavaScript context.
4747

4848
Example:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# BridgeJS Internals
2+
3+
Internal design, performance rationale, and low-level details for BridgeJS.
4+
5+
## Overview
6+
7+
This section is for maintainers and contributors who want to understand how BridgeJS works under the hood, why it is designed the way it is, and how it interacts with the JavaScript engine and ABI.
8+
9+
## Topics
10+
11+
- <doc:Design-Rationale>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# BridgeJS Design Rationale
2+
3+
Why BridgeJS is faster than dynamic `JSObject`/`JSValue` APIs and how engine optimizations influence the design.
4+
5+
## Overview
6+
7+
BridgeJS generates **specialized** bridge code per exported or imported interface. That specialization, combined with stable call and property access patterns, allows JavaScript engines to optimize the boundary crossing much better than with generic dynamic code. This page explains the main performance rationale.
8+
9+
## Why generated code is faster
10+
11+
1. **Specialized code per interface** - Each bridged function or property gets its own glue path with known types. The engine does not need to handle arbitrary shapes or types at the call site.
12+
13+
2. **Use of static type information** - The generator knows parameter and return types at compile time. It can avoid dynamic type checks and boxing where the dynamic API would require them.
14+
15+
3. **IC-friendly access patterns** - Property and method accesses use stable, predictable shapes instead of a single generic subscript path. That keeps engine **inline caches (ICs)** effective instead of turning them **megamorphic**.
16+
17+
## Inline caches (ICs) and megamorphic penalty
18+
19+
JavaScript engines (and many other dynamic-language VMs) use **inline caches** at property and method access sites: they remember the object shape (e.g. “this property is at offset X”) so the next access with the same shape can take a fast path.
20+
21+
- **Monomorphic** - One shape seen at a site → very fast, offset cached.
22+
- **Polymorphic** - A few shapes → still fast, small dispatch in the IC.
23+
- **Megamorphic** - Too many different shapes at the same site → the IC gives up and falls back to a generic property lookup, which is much slower.
24+
25+
Engines typically allow only a small number of shapes per IC (e.g. on the order of a few) before marking the site megamorphic.
26+
27+
## Why `JSObject` subscript is problematic
28+
29+
`JSObject.subscript` (and similar dynamic property access) shares **one** code path for all property names and all object shapes. Every access goes through the same call site with varying keys and receiver shapes. That site therefore sees many different shapes and quickly becomes **megamorphic**, so the engine cannot cache property offsets and must do a generic lookup every time.
30+
31+
So even if you cache the property name (e.g. with `CachedJSStrings`), you are still using the same generic subscript path; the call site stays megamorphic and pays the slow-path cost.
32+
33+
BridgeJS avoids this by generating **separate** access paths per property or method. Each generated getter/setter or function call has a stable shape at the engine level, so the IC can stay monomorphic or polymorphic and the fast path is used.
34+
35+
## What to read next
36+
37+
- ABI and binary interface details will be documented in this section as they stabilize.
38+
- For using BridgeJS in your app, see <doc:Introducing-BridgeJS>, <doc:Importing-JavaScript-into-Swift>, and <doc:Exporting-Swift-to-JavaScript>.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Bringing Swift Closures to JavaScript
2+
3+
Use ``JSTypedClosure`` to pass or return Swift closures to the JavaScript world with BridgeJS-with type safety and explicit lifetime management.
4+
5+
## Overview
6+
7+
``JSTypedClosure`` wraps a **Swift closure** so you can **pass it or return it to JavaScript** through BridgeJS. The closure lives in Swift; JavaScript receives a function that calls back into that closure when invoked. Use it when:
8+
9+
- You **pass** a Swift closure as an argument to a JavaScript API (e.g. an ``JSFunction(jsName:from:)`` that takes a callback parameter).
10+
- You **return** a Swift closure from Swift exported by ``JS(namespace:enumStyle:)`` so JavaScript can call it later.
11+
12+
Unlike ``JSClosure``, which uses untyped ``JSValue`` arguments and return values, ``JSTypedClosure`` has a concrete **signature** (e.g. `(Int) -> Int` or `(String) -> Void`). BridgeJS generates the glue code for that signature, so you get compile-time type safety when crossing into the JS world.
13+
14+
You **must call** ``JSTypedClosure/release()`` when the closure is no longer needed by JavaScript. After release, any attempt to invoke the closure from JavaScript throws an explicit JS exception.
15+
16+
## Creating a JSTypedClosure
17+
18+
BridgeJS generates an initializer for each closure signature used in your module. Wrap your Swift closure and pass or return it to JavaScript:
19+
20+
```swift
21+
import JavaScriptKit
22+
23+
// Pass a Swift closure to a JS function that expects a callback (Int) -> Int
24+
@JSFunction static func applyTransform(_ value: Int, _ transform: JSTypedClosure<(Int) -> Int>) throws(JSException) -> Int
25+
26+
let double = JSTypedClosure<(Int) -> Int> { $0 * 2 }
27+
defer { double.release() }
28+
let result = try applyTransform(10, double) // 20 - JS calls back into Swift
29+
```
30+
31+
You can pass or return typed closures with other signatures the same way:
32+
33+
```swift
34+
let sum = JSTypedClosure<(Int, Int) -> Int> { $0 + $1 }
35+
defer { sum.release() }
36+
37+
let log = JSTypedClosure<(String) -> Void> { print($0) }
38+
defer { log.release() }
39+
```
40+
41+
## Lifetime and release()
42+
43+
A ``JSTypedClosure`` keeps the Swift closure alive and exposes a JavaScript function that calls into it. To avoid leaks and use-after-free:
44+
45+
1. **Call `release()` exactly once** when the closure is no longer needed by JavaScript (e.g. when the callback is unregistered or the object that held it is released).
46+
2. Prefer **`defer { closure.release() }`** right after creating the closure so cleanup runs when the current scope exits.
47+
3. After `release()`, calling the closure from JavaScript throws an exception with a message that includes the file and line where the ``JSTypedClosure`` was created.
48+
49+
A **FinalizationRegistry** on the JavaScript side may eventually release the Swift storage if you never call `release()`, but that is non-deterministic. Do not rely on it for timely cleanup.
50+
51+
## Getting the underlying JavaScript function
52+
53+
When you need to store or pass the function on the JavaScript side (e.g. to compare identity or attach to a DOM node), use the ``JSTypedClosure/jsObject`` property to get the ``JSObject`` that represents the JavaScript function.
54+
55+
## JSTypedClosure vs JSClosure
56+
57+
Both let you pass or return a Swift closure to the JavaScript world. The difference is how they are typed and which API you use:
58+
59+
| | JSTypedClosure | JSClosure |
60+
|:--|:--|:--|
61+
| **API** | BridgeJS (macros, generated code) | Dynamic ``JSObject`` / ``JSValue`` |
62+
| **Types** | Typed signature, e.g. `(Int) -> Int` | Untyped `([JSValue]) -> JSValue` |
63+
| **Lifetime** | Explicit `release()` required | Explicit `release()` required |
64+
| **Use case** | Passing/returning closures at the BridgeJS boundary with a fixed signature | Passing Swift functions to JS via dynamic APIs (e.g. DOM events) |
65+
66+
Use ``JSTypedClosure`` when you pass or return closures through BridgeJS-declared APIs. Use ``JSClosure`` when you pass a Swift function to JavaScript using the dynamic APIs (e.g. ``JSObject``, ``JSValue``) without generated glue.
67+
68+
## JSTypedClosure vs auto-managed closures
69+
70+
BridgeJS can also expose **plain** Swift closure types (e.g. `(String) -> String`) as parameters and return values; lifetime is then managed automatically via ``FinalizationRegistry`` and no `release()` is required. See <doc:Exporting-Swift-Closure>.
71+
72+
**When returning** a closure from Swift to JavaScript, we **recommend** using ``JSTypedClosure`` and managing lifetime explicitly with `release()`, rather than returning a plain closure type. Explicit release makes cleanup predictable and avoids relying solely on JavaScript GC.
73+
74+
## See also
75+
76+
- ``JSTypedClosure``
77+
- ``JSClosure``
78+
- <doc:Exporting-Swift-Closure>
79+
- <doc:Importing-JavaScript-into-Swift>
80+
- <doc:Supported-Types>

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md

Lines changed: 7 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,60 +6,11 @@ Learn how to make your Swift code callable from JavaScript.
66

77
> Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases.
88
9-
> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
10-
11-
BridgeJS allows you to expose Swift functions, classes, and methods to JavaScript by using the `@JS` attribute. This enables JavaScript code to call into Swift code running in WebAssembly.
12-
13-
## Configuring the BridgeJS plugin
14-
15-
To use the BridgeJS feature, you need to enable the experimental `Extern` feature and add the BridgeJS plugin to your package. Here's an example of a `Package.swift` file:
16-
17-
```swift
18-
// swift-tools-version:6.0
19-
20-
import PackageDescription
21-
22-
let package = Package(
23-
name: "MyApp",
24-
dependencies: [
25-
.package(url: "https://github.com/swiftwasm/JavaScriptKit.git", branch: "main")
26-
],
27-
targets: [
28-
.executableTarget(
29-
name: "MyApp",
30-
dependencies: ["JavaScriptKit"],
31-
swiftSettings: [
32-
// This is required because the generated code depends on @_extern(wasm)
33-
.enableExperimentalFeature("Extern")
34-
],
35-
plugins: [
36-
// Add build plugin for processing @JS and generate Swift glue code
37-
.plugin(name: "BridgeJS", package: "JavaScriptKit")
38-
]
39-
)
40-
]
41-
)
42-
```
43-
44-
The `BridgeJS` plugin will process your Swift code to find declarations marked with `@JS` and generate the necessary bridge code to make them accessible from JavaScript.
45-
46-
### Building your package for JavaScript
47-
48-
After configuring your `Package.swift`, you can build your package for JavaScript using the following command:
49-
50-
```bash
51-
swift package --swift-sdk $SWIFT_SDK_ID js
52-
```
53-
54-
This command will:
55-
56-
1. Process all Swift files with `@JS` annotations
57-
2. Generate JavaScript bindings and TypeScript type definitions (`.d.ts`) for your exported Swift code
58-
3. Output everything to the `.build/plugins/PackageToJS/outputs/` directory
59-
60-
> Note: For larger projects, you may want to generate the BridgeJS code ahead of time to improve build performance. See <doc:Ahead-of-Time-Code-Generation> for more information.
9+
> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
6110
11+
BridgeJS allows you to expose Swift functions, classes, and methods to JavaScript by using the ``JS(namespace:enumStyle:)`` attribute. This enables JavaScript code to call into Swift code running in WebAssembly.
6212

13+
Configure your package and build for JavaScript as described in <doc:Setting-up-BridgeJS>. Then use the topics below to expose Swift types and functions to JavaScript.
6314

6415
## Topics
6516

@@ -75,3 +26,7 @@ This command will:
7526
- <doc:Exporting-Swift-Static-Functions>
7627
- <doc:Exporting-Swift-Static-Properties>
7728
- <doc:Using-Namespace>
29+
30+
## See Also
31+
32+
- ``JS(namespace:enumStyle:)``

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Array.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Learn how to pass Swift arrays to and from JavaScript.
44

55
## Overview
66

7-
> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
7+
> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
88
99
BridgeJS allows you to pass Swift arrays as function parameters and return values.
1010

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Learn how to export Swift classes to JavaScript.
44

55
## Overview
66

7-
> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
7+
> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
88
99
To export a Swift class, mark both the class and any members you want to expose:
1010

@@ -51,6 +51,7 @@ cart.addItem("Laptop", 999.99, 1);
5151
cart.addItem("Mouse", 24.99, 2);
5252
console.log(`Items in cart: ${cart.getItemCount()}`);
5353
console.log(`Total: $${cart.getTotal().toFixed(2)}`);
54+
cart.release(); // Call release() when done; don't rely on FinalizationRegistry as much as possible
5455
```
5556

5657
The generated TypeScript declarations for this class would look like:
@@ -83,7 +84,7 @@ Classes use **reference semantics** when crossing the Swift/JavaScript boundary:
8384
1. **Object Creation**: When you create a class instance (via `new` in JS), the object lives on the Swift heap
8485
2. **Reference Passing**: JavaScript receives a reference (handle) to the Swift object, not a copy
8586
3. **Shared State**: Changes made through either Swift or JavaScript affect the same object
86-
4. **Memory Management**: `FinalizationRegistry` automatically releases Swift objects when they're garbage collected in JavaScript. You can optionally call `release()` for deterministic cleanup.
87+
4. **Memory Management**: `FinalizationRegistry` can release Swift objects when they're garbage collected in JavaScript, but you should **not rely on it** for cleanup. Call `release()` when an instance is no longer needed so that Swift memory is reclaimed deterministically. This is especially important for **short-lived instances**: GC may run late or not at all for objects that become unreachable quickly, so relying on `FinalizationRegistry` can delay or leak Swift-side resources.
8788
8889
This differs from structs, which use copy semantics and transfer data by value.
8990

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ Learn how to use closure/function types as parameters and return values in Bridg
44

55
## Overview
66

7-
> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
7+
> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/).
88
9-
BridgeJS supports typed closure parameters and return values, allowing you to pass functions between Swift and JavaScript with full type safety. This enables functional programming patterns like callbacks, higher-order functions, and function composition across the language boundary.
9+
BridgeJS supports typed closure parameters and return values, allowing you to pass or return Swift closures to JavaScript with full type safety. **Lifetime is automatic**: you use plain Swift closure types (e.g. `(String) -> String`); the runtime releases them when no longer needed-no manual `release()` required. This enables callbacks, higher-order functions, and function composition across the boundary.
10+
11+
**Recommendation:** When **returning** a closure from Swift to JavaScript, prefer returning a ``JSTypedClosure`` and managing its lifetime explicitly (see <doc:Bringing-Swift-Closures-to-JavaScript>). Explicit `release()` makes cleanup predictable and avoids relying solely on JavaScript garbage collection. Use plain closure types (this article) when you want fully automatic lifetime or when passing closures only as parameters into your exported API.
1012

1113
## Example
1214

@@ -99,6 +101,8 @@ Closures use **reference semantics** when crossing the Swift/JavaScript boundary
99101
100102
This differs from structs and arrays, which use copy semantics and transfer data by value.
101103
104+
When you **return** a closure to JavaScript, we recommend using ``JSTypedClosure`` and calling `release()` when the closure is no longer needed, instead of returning a plain closure type. See <doc:Bringing-Swift-Closures-to-JavaScript>.
105+
102106
## Supported Features
103107
104108
| Swift Feature | Status |
@@ -113,4 +117,5 @@ This differs from structs and arrays, which use copy semantics and transfer data
113117
114118
## See Also
115119
120+
- <doc:Bringing-Swift-Closures-to-JavaScript> - passing or returning closures with ``JSTypedClosure`` and explicit `release()`
116121
- <doc:Supported-Types>

0 commit comments

Comments
 (0)