Skip to content

Commit 5a7fed5

Browse files
Add escape hatch for old environments
1 parent 43a33ee commit 5a7fed5

File tree

4 files changed

+110
-63
lines changed

4 files changed

+110
-63
lines changed

Diff for: Runtime/src/index.ts

+34-12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ if (typeof globalThis !== "undefined") {
2121

2222
interface SwiftRuntimeExportedFunctions {
2323
swjs_library_version(): number;
24+
swjs_library_features(): number;
2425
swjs_prepare_host_function_call(size: number): pointer;
2526
swjs_cleanup_host_function_call(argv: pointer): void;
2627
swjs_call_host_function(
@@ -44,6 +45,10 @@ enum JavaScriptValueKind {
4445
Function = 6,
4546
}
4647

48+
enum LibraryFeatures {
49+
WeakRefs = 1 << 0,
50+
}
51+
4752
type TypedArray =
4853
| Int8ArrayConstructor
4954
| Uint8ArrayConstructor
@@ -117,25 +122,33 @@ class SwiftRuntimeHeap {
117122
}
118123
}
119124

125+
/// Memory lifetime of closures in Swift are managed by Swift side
126+
class SwiftClosureHeap {
127+
private functionRegistry: FinalizationRegistry<number>;
128+
private exports: SwiftRuntimeExportedFunctions
129+
130+
constructor(exports: SwiftRuntimeExportedFunctions) {
131+
this.exports = exports
132+
this.functionRegistry = new FinalizationRegistry((id) => {
133+
this.exports.swjs_free_host_function(id);
134+
});
135+
}
136+
137+
alloc(func: any, func_ref: number) {
138+
this.functionRegistry.register(func, func_ref);
139+
}
140+
}
141+
120142
export class SwiftRuntime {
121143
private instance: WebAssembly.Instance | null;
122144
private heap: SwiftRuntimeHeap;
123-
private functionRegistry: FinalizationRegistry<unknown>;
145+
private closureHeap: SwiftClosureHeap | null;
124146
private version: number = 701;
125147

126148
constructor() {
127149
this.instance = null;
128150
this.heap = new SwiftRuntimeHeap();
129-
this.functionRegistry = new FinalizationRegistry(
130-
this.handleFree.bind(this)
131-
);
132-
}
133-
134-
handleFree(id: unknown) {
135-
if (!this.instance || typeof id !== "number") return;
136-
const exports = (this.instance
137-
.exports as any) as SwiftRuntimeExportedFunctions;
138-
exports.swjs_free_host_function(id);
151+
this.closureHeap = null;
139152
}
140153

141154
setInstance(instance: WebAssembly.Instance) {
@@ -145,6 +158,15 @@ export class SwiftRuntime {
145158
if (exports.swjs_library_version() != this.version) {
146159
throw new Error("The versions of JavaScriptKit are incompatible.");
147160
}
161+
const features = exports.swjs_library_features();
162+
const librarySupportsWeakRef = features & LibraryFeatures.WeakRefs;
163+
if (librarySupportsWeakRef) {
164+
if (typeof FinalizationRegistry !== "undefined") {
165+
this.closureHeap = new SwiftClosureHeap(exports);
166+
} else {
167+
throw new Error("The JavaScriptKit in Swift expects the target environment supports WeakRefs. Please build with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to disable features using WeakRefs.");
168+
}
169+
}
148170
}
149171

150172
importObjects() {
@@ -472,7 +494,7 @@ export class SwiftRuntime {
472494
);
473495
};
474496
const func_ref = this.heap.retain(func);
475-
this.functionRegistry.register(func, func_ref);
497+
this.closureHeap?.alloc(func, func_ref);
476498
writeUint32(func_ref_ptr, func_ref);
477499
},
478500
swjs_call_throwing_new: (

Diff for: Sources/JavaScriptKit/Features.swift

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
struct LibraryFeatures: OptionSet {
2+
let rawValue: Int32
3+
4+
static let weakRefs = LibraryFeatures(rawValue: 1 << 0)
5+
}
6+
7+
@_cdecl("_library_features")
8+
func _library_features() -> Int32 {
9+
var features: LibraryFeatures = []
10+
#if !JAVASCRIPTKIT_WITHOUT_WEAKREFS
11+
features.insert(.weakRefs)
12+
#endif
13+
return features.rawValue
14+
}

Diff for: Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift

+54-50
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import _CJavaScriptKit
22

3-
fileprivate var closureRef: JavaScriptHostFuncRef = 0
4-
fileprivate var sharedClosures: [JavaScriptHostFuncRef: ([JSValue]) -> JSValue] = [:]
5-
6-
/// JSClosureProtocol abstracts closure object in JavaScript, whose lifetime is manualy managed
3+
/// JSClosureProtocol abstracts closure object in JavaScript, whose lifetime might be manualy managed
74
public protocol JSClosureProtocol: JSValueCompatible {
85

96
/// Release this function resource.
@@ -28,8 +25,15 @@ public protocol JSClosureProtocol: JSValueCompatible {
2825
/// ```
2926
///
3027
public class JSClosure: JSObject, JSClosureProtocol {
28+
29+
fileprivate static var sharedClosures: [JavaScriptHostFuncRef: ([JSValue]) -> JSValue] = [:]
30+
3131
private var hostFuncRef: JavaScriptHostFuncRef = 0
3232

33+
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
34+
private var isReleased: Bool = false
35+
#endif
36+
3337
@available(*, deprecated, message: "This initializer will be removed in the next minor version update. Please use `init(_ body: @escaping ([JSValue]) -> JSValue)` and add `return .undefined` to the end of your closure")
3438
@_disfavoredOverload
3539
public convenience init(_ body: @escaping ([JSValue]) -> ()) {
@@ -40,26 +44,27 @@ public class JSClosure: JSObject, JSClosureProtocol {
4044
}
4145

4246
public init(_ body: @escaping ([JSValue]) -> JSValue) {
43-
self.hostFuncRef = closureRef
44-
closureRef += 1
45-
46-
// Retain the given body in static storage by `closureRef`.
47-
sharedClosures[self.hostFuncRef] = body
48-
49-
// Create a new JavaScript function which calls the given Swift function.
47+
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
48+
super.init(id: 0)
49+
let objectId = ObjectIdentifier(self)
50+
let funcRef = JavaScriptHostFuncRef(bitPattern: Int32(objectId.hashValue))
51+
// 2. Retain the given body in static storage by `funcRef`.
52+
Self.sharedClosures[funcRef] = body
53+
// 3. Create a new JavaScript function which calls the given Swift function.
5054
var objectRef: JavaScriptObjectRef = 0
51-
_create_function(self.hostFuncRef, &objectRef)
55+
_create_function(funcRef, &objectRef)
5256

53-
super.init(id: objectRef)
57+
hostFuncRef = funcRef
58+
id = objectRef
5459
}
5560

56-
@available(*, deprecated, message: "JSClosure.release() is no longer necessary")
57-
public func release() {}
58-
}
59-
60-
@_cdecl("_free_host_function_impl")
61-
func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {
62-
sharedClosures[hostFuncRef] = nil
61+
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
62+
deinit {
63+
guard isReleased else {
64+
fatalError("release() must be called on JSClosure objects manually before they are deallocated")
65+
}
66+
}
67+
#endif
6368
}
6469

6570

@@ -103,7 +108,7 @@ func _call_host_function_impl(
103108
_ argv: UnsafePointer<RawJSValue>, _ argc: Int32,
104109
_ callbackFuncRef: JavaScriptObjectRef
105110
) {
106-
guard let hostFunc = sharedClosures[hostFuncRef] else {
111+
guard let hostFunc = JSClosure.sharedClosures[hostFuncRef] else {
107112
fatalError("The function was already released")
108113
}
109114
let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map {
@@ -114,10 +119,16 @@ func _call_host_function_impl(
114119
_ = callbackFuncRef(result)
115120
}
116121

122+
123+
124+
// [WeakRefs](https://github.com/tc39/proposal-weakrefs) is already Stage 4, but it's still newish API,
125+
// so provide a escape hatch.
126+
// Please build with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` by SwiftPM build system
127+
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
128+
117129
// MARK: - Legacy Closure Types
118130

119131
/// `JSOneshotClosure` is a JavaScript function that can be called only once.
120-
/// It is recommended to use `JSClosure` instead if your target runtimes support `FinalizationRegistry`.
121132
public class JSOneshotClosure: JSObject, JSClosureProtocol {
122133
private var hostFuncRef: JavaScriptHostFuncRef = 0
123134

@@ -127,7 +138,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
127138
let objectId = ObjectIdentifier(self)
128139
let funcRef = JavaScriptHostFuncRef(bitPattern: Int32(objectId.hashValue))
129140
// 2. Retain the given body in static storage by `funcRef`.
130-
sharedClosures[funcRef] = {
141+
JSClosure.sharedClosures[funcRef] = {
131142
defer { self.release() }
132143
return body($0)
133144
}
@@ -142,38 +153,31 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
142153
/// Release this function resource.
143154
/// After calling `release`, calling this function from JavaScript will fail.
144155
public func release() {
145-
sharedClosures[hostFuncRef] = nil
156+
JSClosure.sharedClosures[hostFuncRef] = nil
146157
}
147158
}
148159

149-
public class JSUnretainedClosure: JSObject, JSClosureProtocol {
150-
private var hostFuncRef: JavaScriptHostFuncRef = 0
151-
var isReleased: Bool = false
152-
153-
public init(_ body: @escaping ([JSValue]) -> JSValue) {
154-
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
155-
super.init(id: 0)
156-
let objectId = ObjectIdentifier(self)
157-
let funcRef = JavaScriptHostFuncRef(bitPattern: Int32(objectId.hashValue))
158-
// 2. Retain the given body in static storage by `funcRef`.
159-
sharedClosures[funcRef] = body
160-
// 3. Create a new JavaScript function which calls the given Swift function.
161-
var objectRef: JavaScriptObjectRef = 0
162-
_create_function(funcRef, &objectRef)
163-
164-
hostFuncRef = funcRef
165-
id = objectRef
166-
}
167-
160+
extension JSClosure {
168161
public func release() {
169162
isReleased = true
170-
sharedClosures[hostFuncRef] = nil
163+
Self.sharedClosures[hostFuncRef] = nil
171164
}
165+
}
172166

173-
deinit {
174-
guard isReleased else {
175-
// Safari doesn't support `FinalizationRegistry`, so we cannot automatically manage the lifetime of Swift objects
176-
fatalError("release() must be called on JSClosure objects manually before they are deallocated")
177-
}
178-
}
167+
#else
168+
169+
@available(*, deprecated, renamed: "JSClosure", message: "JSClosure supports automatic memory management")
170+
public typealias JSOneshotClosure = JSClosure
171+
172+
extension JSClosure {
173+
174+
@available(*, deprecated, message: "JSClosure.release() is no longer necessary if the target environment supports WeakRefs")
175+
public func release() {}
176+
177+
}
178+
179+
@_cdecl("_free_host_function_impl")
180+
func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {
181+
JSClosure.sharedClosures[hostFuncRef] = nil
179182
}
183+
#endif

Diff for: Sources/_CJavaScriptKit/_CJavaScriptKit.c

+8-1
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,15 @@ void swjs_cleanup_host_function_call(void *argv_buffer) {
3535
/// Notes: If you change any interface of runtime library, please increment
3636
/// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`.
3737
__attribute__((export_name("swjs_library_version")))
38-
int swjs_library_version() {
38+
int swjs_library_version(void) {
3939
return 701;
4040
}
4141

42+
int _library_features(void);
43+
44+
__attribute__((export_name("swjs_library_features")))
45+
int swjs_library_features(void) {
46+
return _library_features();
47+
}
48+
4249
#endif

0 commit comments

Comments
 (0)