-
-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathJSFunction.swift
More file actions
219 lines (198 loc) · 9.19 KB
/
JSFunction.swift
File metadata and controls
219 lines (198 loc) · 9.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import _CJavaScriptKit
/// `JSFunction` represents a function in JavaScript and supports new object instantiation.
/// This type can be callable as a function using `callAsFunction`.
///
/// e.g.
/// ```swift
/// let alert: JSFunction = JSObject.global.alert.function!
/// // Call `JSFunction` as a function
/// alert("Hello, world")
/// ```
///
public class JSFunction: JSObject {
/// Call this function with given `arguments` and binding given `this` as context.
/// - Parameters:
/// - this: The value to be passed as the `this` parameter to this function.
/// - arguments: Arguments to be passed to this function.
/// - Returns: The result of this call.
@discardableResult
public func callAsFunction(this: JSObject? = nil, arguments: [JSValueConvertible]) -> JSValue {
let result = arguments.withRawJSValues { rawValues in
rawValues.withUnsafeBufferPointer { bufferPointer -> RawJSValue in
let argv = bufferPointer.baseAddress
let argc = bufferPointer.count
var result = RawJSValue()
if let thisId = this?.id {
_call_function_with_this(thisId,
self.id, argv, Int32(argc),
&result.kind, &result.payload1, &result.payload2)
} else {
_call_function(
self.id, argv, Int32(argc),
&result.kind, &result.payload1, &result.payload2
)
}
return result
}
}
return result.jsValue()
}
/// A variadic arguments version of `callAsFunction`.
@discardableResult
public func callAsFunction(this: JSObject? = nil, _ arguments: JSValueConvertible...) -> JSValue {
self(this: this, arguments: arguments)
}
/// Instantiate an object from this function as a constructor.
///
/// Guaranteed to return an object because either:
///
/// - a. the constructor explicitly returns an object, or
/// - b. the constructor returns nothing, which causes JS to return the `this` value, or
/// - c. the constructor returns undefined, null or a non-object, in which case JS also returns `this`.
///
/// - Parameter arguments: Arguments to be passed to this constructor function.
/// - Returns: A new instance of this constructor.
public func new(arguments: [JSValueConvertible]) -> JSObject {
arguments.withRawJSValues { rawValues in
rawValues.withUnsafeBufferPointer { bufferPointer in
let argv = bufferPointer.baseAddress
let argc = bufferPointer.count
var resultObj = JavaScriptObjectRef()
_call_new(self.id, argv, Int32(argc), &resultObj)
return JSObject(id: resultObj)
}
}
}
/// A variadic arguments version of `new`.
public func new(_ arguments: JSValueConvertible...) -> JSObject {
new(arguments: arguments)
}
@available(*, unavailable, message: "Please use JSClosure instead")
public static func from(_: @escaping ([JSValue]) -> JSValue) -> JSFunction {
fatalError("unavailable")
}
public override class func construct(from value: JSValue) -> Self? {
return value.function as? Self
}
override public func jsValue() -> JSValue {
.function(self)
}
}
/// `JSClosure` represents a JavaScript function the body of which is written in Swift.
/// This type can be passed as a callback handler to JavaScript functions.
/// Note that the lifetime of `JSClosure` should be managed by users manually
/// due to GC boundary between Swift and JavaScript.
/// For further discussion, see also [swiftwasm/JavaScriptKit #33](https://github.com/swiftwasm/JavaScriptKit/pull/33)
///
/// e.g.
/// ```swift
/// let eventListenter = JSClosure { _ in
/// ...
/// return JSValue.undefined
/// }
///
/// button.addEventListener!("click", JSValue.function(eventListenter))
/// ...
/// button.removeEventListener!("click", JSValue.function(eventListenter))
/// eventListenter.release()
/// ```
///
public class JSClosure: JSFunction {
static var sharedFunctions: [JavaScriptHostFuncRef: ([JSValue]) -> JSValue] = [:]
private var hostFuncRef: JavaScriptHostFuncRef = 0
private var isReleased = false
/// Instantiate a new `JSClosure` with given function body.
/// - Parameter body: The body of this function.
public init(_ body: @escaping ([JSValue]) -> JSValue) {
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
super.init(id: 0)
let objectId = ObjectIdentifier(self)
let funcRef = JavaScriptHostFuncRef(bitPattern: Int32(objectId.hashValue))
// 2. Retain the given body in static storage by `funcRef`.
Self.sharedFunctions[funcRef] = body
// 3. Create a new JavaScript function which calls the given Swift function.
var objectRef: JavaScriptObjectRef = 0
_create_function(funcRef, &objectRef)
hostFuncRef = funcRef
id = objectRef
}
/// A convenience initializer which assumes that the given body function returns `JSValue.undefined`
convenience public init(_ body: @escaping ([JSValue]) -> ()) {
self.init { (arguments: [JSValue]) -> JSValue in
body(arguments)
return .undefined
}
}
/// Release this function resource.
/// After calling `release`, calling this function from JavaScript will fail.
public func release() {
Self.sharedFunctions[hostFuncRef] = nil
isReleased = true
}
deinit {
guard isReleased else {
fatalError("""
release() must be called on closures manually before deallocating.
This is caused by the lack of support for the `FinalizationRegistry` API in Safari.
""")
}
}
}
// MARK: - `JSClosure` mechanism note
//
// 1. Create thunk function in JavaScript world, that has a reference
// to Swift Closure.
// ┌─────────────────────┬──────────────────────────┐
// │ Swift side │ JavaScript side │
// │ │ │
// │ │ │
// │ │ ┌──[Thunk function]──┐ │
// │ ┌ ─ ─ ─ ─ ─│─ ─│─ ─ ─ ─ ─ ┐ │ │
// │ ↓ │ │ │ │ │
// │ [Swift Closure] │ │ Host Function ID │ │
// │ │ │ │ │
// │ │ └────────────────────┘ │
// └─────────────────────┴──────────────────────────┘
//
// 2. When thunk function is invoked, it calls Swift Closure via
// `_call_host_function` and callback the result through callback func
// ┌─────────────────────┬──────────────────────────┐
// │ Swift side │ JavaScript side │
// │ │ │
// │ │ │
// │ Apply ┌──[Thunk function]──┐ │
// │ ┌ ─ ─ ─ ─ ─│─ ─│─ ─ ─ ─ ─ ┐ │ │
// │ ↓ │ │ │ │ │
// │ [Swift Closure] │ │ Host Function ID │ │
// │ │ │ │ │ │
// │ │ │ └────────────────────┘ │
// │ │ │ ↑ │
// │ │ Apply │ │
// │ └─[Result]─┼───>[Callback func]─┘ │
// │ │ │
// └─────────────────────┴──────────────────────────┘
@_cdecl("swjs_prepare_host_function_call")
func _prepare_host_function_call(_ argc: Int32) -> UnsafeMutableRawPointer {
let argumentSize = MemoryLayout<RawJSValue>.size * Int(argc)
return malloc(Int(argumentSize))!
}
@_cdecl("swjs_cleanup_host_function_call")
func _cleanup_host_function_call(_ pointer: UnsafeMutableRawPointer) {
free(pointer)
}
@_cdecl("swjs_call_host_function")
func _call_host_function(
_ hostFuncRef: JavaScriptHostFuncRef,
_ argv: UnsafePointer<RawJSValue>, _ argc: Int32,
_ callbackFuncRef: JavaScriptObjectRef
) {
guard let hostFunc = JSClosure.sharedFunctions[hostFuncRef] else {
fatalError("The function was already released")
}
let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map {
$0.jsValue()
}
let result = hostFunc(arguments)
let callbackFuncRef = JSFunction(id: callbackFuncRef)
_ = callbackFuncRef(result)
}