@@ -19,17 +19,15 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
1919 // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
2020 super. init ( id: 0 )
2121
22- // 2. Create a new JavaScript function which calls the given Swift function.
23- hostFuncRef = JavaScriptHostFuncRef ( bitPattern: ObjectIdentifier ( self ) )
24- id = withExtendedLifetime ( JSString ( file) ) { file in
25- _create_function ( hostFuncRef, line, file. asInternalJSRef ( ) )
26- }
27-
28- // 3. Retain the given body in static storage by `funcRef`.
29- JSClosure . sharedClosures [ hostFuncRef] = ( self , {
22+ // 2. Retain the given body in static storage
23+ // Leak the self object globally and release once it's called
24+ hostFuncRef = JSClosure . sharedClosures. register ( ObjectIdentifier ( self ) , object: . strong( self ) , body: {
3025 defer { self . release ( ) }
3126 return body ( $0)
3227 } )
28+ id = withExtendedLifetime ( JSString ( file) ) { file in
29+ _create_function ( hostFuncRef, line, file. asInternalJSRef ( ) )
30+ }
3331 }
3432
3533 #if compiler(>=5.5)
@@ -42,7 +40,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
4240 /// Release this function resource.
4341 /// After calling `release`, calling this function from JavaScript will fail.
4442 public func release( ) {
45- JSClosure . sharedClosures [ hostFuncRef] = nil
43+ JSClosure . sharedClosures. unregister ( hostFuncRef)
4644 }
4745}
4846
@@ -62,13 +60,13 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
6260/// ```
6361///
6462public class JSClosure : JSFunction , JSClosureProtocol {
65-
66- // Note: Retain the closure object itself also to avoid funcRef conflicts
67- fileprivate static var sharedClosures : [ JavaScriptHostFuncRef : ( object: JSObject , body: ( [ JSValue ] ) -> JSValue ) ] = [ : ]
63+ fileprivate static var sharedClosures = SharedJSClosureRegistry ( )
6864
6965 private var hostFuncRef : JavaScriptHostFuncRef = 0
7066
7167 #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
68+ private let file : String
69+ private let line : UInt32
7270 private var isReleased : Bool = false
7371 #endif
7472
@@ -82,30 +80,35 @@ public class JSClosure: JSFunction, JSClosureProtocol {
8280 }
8381
8482 public init ( _ body: @escaping ( [ JSValue ] ) -> JSValue , file: String = #fileID, line: UInt32 = #line) {
83+ #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
84+ self . file = file
85+ self . line = line
86+ #endif
8587 // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
8688 super. init ( id: 0 )
8789
88- // 2. Create a new JavaScript function which calls the given Swift function.
89- hostFuncRef = JavaScriptHostFuncRef ( bitPattern: ObjectIdentifier ( self ) )
90+ // 2. Retain the given body in static storage
91+ hostFuncRef = Self . sharedClosures. register (
92+ ObjectIdentifier ( self ) , object: . weak( self ) , body: body
93+ )
94+
9095 id = withExtendedLifetime ( JSString ( file) ) { file in
9196 _create_function ( hostFuncRef, line, file. asInternalJSRef ( ) )
9297 }
9398
94- // 3. Retain the given body in static storage by `funcRef`.
95- Self . sharedClosures [ hostFuncRef] = ( self , body)
9699 }
97100
98101 #if compiler(>=5.5)
99102 @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
100- public static func async ( _ body: @escaping ( [ JSValue ] ) async throws -> JSValue ) -> JSClosure {
101- JSClosure ( makeAsyncClosure ( body) )
103+ public static func async ( _ body: @escaping ( [ JSValue ] ) async throws -> JSValue , file : String = #fileID , line : UInt32 = #line ) -> JSClosure {
104+ JSClosure ( makeAsyncClosure ( body) , file : file , line : line )
102105 }
103106 #endif
104107
105108 #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
106109 deinit {
107110 guard isReleased else {
108- fatalError ( " release() must be called on JSClosure objects manually before they are deallocated " )
111+ fatalError ( " release() must be called on JSClosure object ( \( file ) : \( line ) ) manually before they are deallocated" )
109112 }
110113 }
111114 #endif
@@ -133,6 +136,60 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa
133136}
134137#endif
135138
139+ /// Registry for Swift closures that are referenced from JavaScript.
140+ private struct SharedJSClosureRegistry {
141+ struct ClosureEntry {
142+ // Note: Retain the closure object itself also to avoid funcRef conflicts.
143+ var object : AnyObjectReference
144+ var body : ( [ JSValue ] ) -> JSValue
145+
146+ init ( object: AnyObjectReference , body: @escaping ( [ JSValue ] ) -> JSValue ) {
147+ self . object = object
148+ self . body = body
149+ }
150+ }
151+ enum AnyObjectReference {
152+ case strong( AnyObject )
153+ case weak( WeakObject )
154+
155+ static func `weak`( _ object: AnyObject ) -> AnyObjectReference {
156+ . weak( SharedJSClosureRegistry . WeakObject ( underlying: object) )
157+ }
158+ }
159+ struct WeakObject {
160+ weak var underlying : AnyObject ?
161+ init ( underlying: AnyObject ) {
162+ self . underlying = underlying
163+ }
164+ }
165+ private var closures : [ JavaScriptHostFuncRef : ClosureEntry ] = [ : ]
166+
167+ /// Register a Swift closure to be called from JavaScript.
168+ /// - Parameters:
169+ /// - hint: A hint to identify the closure.
170+ /// - object: The object should be retained until the closure is released from JavaScript.
171+ /// - body: The closure to be called from JavaScript.
172+ /// - Returns: An unique identifier for the registered closure.
173+ mutating func register(
174+ _ hint: ObjectIdentifier ,
175+ object: AnyObjectReference , body: @escaping ( [ JSValue ] ) -> JSValue
176+ ) -> JavaScriptHostFuncRef {
177+ let ref = JavaScriptHostFuncRef ( bitPattern: hint)
178+ closures [ ref] = ClosureEntry ( object: object, body: body)
179+ return ref
180+ }
181+
182+ /// Unregister a Swift closure from the registry.
183+ mutating func unregister( _ ref: JavaScriptHostFuncRef ) {
184+ closures [ ref] = nil
185+ }
186+
187+ /// Get the Swift closure from the registry.
188+ subscript( _ ref: JavaScriptHostFuncRef ) -> ( ( [ JSValue ] ) -> JSValue ) ? {
189+ closures [ ref] ? . body
190+ }
191+ }
192+
136193// MARK: - `JSClosure` mechanism note
137194//
138195// 1. Create a thunk in the JavaScript world, which has a reference
@@ -174,7 +231,7 @@ func _call_host_function_impl(
174231 _ argv: UnsafePointer < RawJSValue > , _ argc: Int32 ,
175232 _ callbackFuncRef: JavaScriptObjectRef
176233) -> Bool {
177- guard let ( _ , hostFunc) = JSClosure . sharedClosures [ hostFuncRef] else {
234+ guard let hostFunc = JSClosure . sharedClosures [ hostFuncRef] else {
178235 return true
179236 }
180237 let arguments = UnsafeBufferPointer ( start: argv, count: Int ( argc) ) . map ( \. jsValue)
@@ -195,7 +252,7 @@ func _call_host_function_impl(
195252extension JSClosure {
196253 public func release( ) {
197254 isReleased = true
198- Self . sharedClosures [ hostFuncRef] = nil
255+ Self . sharedClosures. unregister ( hostFuncRef)
199256 }
200257}
201258
@@ -213,6 +270,6 @@ extension JSClosure {
213270
214271@_cdecl ( " _free_host_function_impl " )
215272func _free_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef ) {
216- JSClosure . sharedClosures [ hostFuncRef] = nil
273+ JSClosure . sharedClosures. unregister ( hostFuncRef)
217274}
218275#endif
0 commit comments