Skip to content

Commit d164af8

Browse files
Experimental async/await support (#112)
* wip * Add sample * Remove prototype * Apply relative pointer patch for wasm * Update example code to test without capturing context * Remove prototype target * WIP * Update JSPromise interface * Add cxxLanguageStandard option * Strip unused headers * Update toolchain version * Exclude non-source files * Fix example project build * Update copy-header to download from remote source * Adopt JSPromise changes * Split Xcode support code * Add API documents * Adopt JSPromsie API change part2 * Add linguist-vendored * Update README * Revert JSObject changes
1 parent b9984f8 commit d164af8

File tree

101 files changed

+33678
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+33678
-4
lines changed

.gitattribute

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Sources/_CJavaScriptEventLoop/swift/* linguist-vendored
2+
Sources/_CJavaScriptEventLoop/llvm/* linguist-vendored

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
wasm-5.3.0-RELEASE
1+
wasm-DEVELOPMENT-SNAPSHOT-2021-01-10-a

Example/JavaScriptKitExample/Package.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,16 @@ let package = Package(
1010
),
1111
],
1212
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
13-
targets: [.target(name: "JavaScriptKitExample", dependencies: ["JavaScriptKit"])]
13+
targets: [
14+
.target(
15+
name: "JavaScriptKitExample",
16+
dependencies: [
17+
.product(name: "JavaScriptKit", package: "JavaScriptKit"),
18+
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
19+
],
20+
swiftSettings: [
21+
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
22+
]
23+
),
24+
]
1425
)

Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import JavaScriptKit
2+
import JavaScriptEventLoop
3+
4+
JavaScriptEventLoop.install()
25

36
let alert = JSObject.global.alert.function!
47
let document = JSObject.global.document
@@ -15,3 +18,15 @@ let listener = JSClosure { _ in
1518
buttonElement.onclick = .object(listener)
1619

1720
_ = document.body.appendChild(buttonElement)
21+
22+
let fetch = JSObject.global.fetch.function!.async
23+
24+
func printZen() async {
25+
let result = await try! fetch("https://api.github.com/zen").object!
26+
let text = await try! result.asyncing.text!()
27+
print(text)
28+
}
29+
30+
JavaScriptEventLoop.runAsync {
31+
await printZen()
32+
}

IntegrationTests/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ benchmark: benchmark_setup run_benchmark
3131
.PHONY: test
3232
test: build_rt dist/PrimaryTests.wasm
3333
node bin/primary-tests.js
34+
concurrency_test: build_rt dist/ConcurrencyTests.wasm
35+
node bin/concurrency-tests.js

IntegrationTests/TestSuites/Package.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ let package = Package(
1515
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
1616
targets: [
1717
.target(name: "PrimaryTests", dependencies: ["JavaScriptKit"]),
18+
.target(
19+
name: "ConcurrencyTests",
20+
dependencies: [
21+
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
22+
],
23+
swiftSettings: [
24+
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
25+
]
26+
),
1827
.target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]),
1928
]
2029
)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import JavaScriptKit
2+
3+
var printTestNames = false
4+
// Uncomment the next line to print the name of each test suite before running it.
5+
// This will make it easier to debug any errors that occur on the JS side.
6+
//printTestNames = true
7+
8+
func test(_ name: String, testBlock: () throws -> Void) throws {
9+
if printTestNames { print(name) }
10+
do {
11+
try testBlock()
12+
} catch {
13+
print("Error in \(name)")
14+
print(error)
15+
throw error
16+
}
17+
}
18+
19+
func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void {
20+
if printTestNames { print(name) }
21+
do {
22+
await try testBlock()
23+
} catch {
24+
print("Error in \(name)")
25+
print(error)
26+
throw error
27+
}
28+
}
29+
30+
struct MessageError: Error {
31+
let message: String
32+
let file: StaticString
33+
let line: UInt
34+
let column: UInt
35+
init(_ message: String, file: StaticString, line: UInt, column: UInt) {
36+
self.message = message
37+
self.file = file
38+
self.line = line
39+
self.column = column
40+
}
41+
}
42+
43+
func expectEqual<T: Equatable>(
44+
_ lhs: T, _ rhs: T,
45+
file: StaticString = #file, line: UInt = #line, column: UInt = #column
46+
) throws {
47+
if lhs != rhs {
48+
throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column)
49+
}
50+
}
51+
52+
func expectCast<T, U>(
53+
_ value: T, to type: U.Type = U.self,
54+
file: StaticString = #file, line: UInt = #line, column: UInt = #column
55+
) throws -> U {
56+
guard let value = value as? U else {
57+
throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column)
58+
}
59+
return value
60+
}
61+
62+
func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject {
63+
switch value {
64+
case let .object(ref): return ref
65+
default:
66+
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
67+
}
68+
}
69+
70+
func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray {
71+
guard let array = value.array else {
72+
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
73+
}
74+
return array
75+
}
76+
77+
func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction {
78+
switch value {
79+
case let .function(ref): return ref
80+
default:
81+
throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column)
82+
}
83+
}
84+
85+
func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool {
86+
switch value {
87+
case let .boolean(bool): return bool
88+
default:
89+
throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column)
90+
}
91+
}
92+
93+
func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double {
94+
switch value {
95+
case let .number(number): return number
96+
default:
97+
throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column)
98+
}
99+
}
100+
101+
func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String {
102+
switch value {
103+
case let .string(string): return String(string)
104+
default:
105+
throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column)
106+
}
107+
}
108+
109+
func expectAsyncThrow<T>(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error {
110+
do {
111+
_ = await try body()
112+
} catch {
113+
return error
114+
}
115+
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
116+
}
117+
118+
func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws {
119+
switch value {
120+
case .some: return
121+
case .none:
122+
throw MessageError("Expect a non-nil value", file: file, line: line, column: column)
123+
}
124+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import JavaScriptEventLoop
2+
import JavaScriptKit
3+
4+
JavaScriptEventLoop.install()
5+
6+
try JavaScriptEventLoop.runAsync {
7+
struct E: Error, Equatable {
8+
let value: Int
9+
}
10+
11+
await try asyncTest("Task.runDetached value") {
12+
let handle = Task.runDetached { 1 }
13+
await try expectEqual(handle.get(), 1)
14+
}
15+
16+
await try asyncTest("Task.runDetached throws") {
17+
let handle = Task.runDetached {
18+
throw E(value: 2)
19+
}
20+
let error = await try expectAsyncThrow(await handle.get())
21+
let e = try expectCast(error, to: E.self)
22+
try expectEqual(e, E(value: 2))
23+
}
24+
25+
await try asyncTest("await resolved Promise") {
26+
let p = JSPromise(resolver: { resolve in
27+
resolve(.success(1))
28+
})
29+
await try expectEqual(p.await(), 1)
30+
}
31+
32+
await try asyncTest("await rejected Promise") {
33+
let p = JSPromise(resolver: { resolve in
34+
resolve(.failure(.number(3)))
35+
})
36+
let error = await try expectAsyncThrow(await p.await())
37+
let jsValue = try expectCast(error, to: JSValue.self)
38+
try expectEqual(jsValue, 3)
39+
}
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const { startWasiTask } = require("../lib");
2+
3+
global.fetch = require('node-fetch');
4+
global.sleep = function () {
5+
return new Promise(resolve => {
6+
setTimeout(() => {
7+
resolve('resolved');
8+
}, 2000);
9+
});
10+
}
11+
12+
startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => {
13+
console.log(err);
14+
process.exit(1);
15+
});

Package.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:5.3
22

33
import PackageDescription
44

55
let package = Package(
66
name: "JavaScriptKit",
77
products: [
88
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
9+
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
910
],
1011
targets: [
1112
.target(
1213
name: "JavaScriptKit",
1314
dependencies: ["_CJavaScriptKit"]
1415
),
16+
.target(
17+
name: "JavaScriptEventLoop",
18+
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"],
19+
swiftSettings: [
20+
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
21+
]
22+
),
1523
.target(name: "_CJavaScriptKit"),
16-
]
24+
.target(
25+
name: "_CJavaScriptEventLoop",
26+
dependencies: ["_CJavaScriptKit"],
27+
exclude: [
28+
"README", "LICENSE-llvm", "LICENSE-swift", "scripts",
29+
"include/swift/ABI/MetadataKind.def",
30+
"include/swift/ABI/ValueWitness.def",
31+
"include/swift/AST/ReferenceStorage.def",
32+
"include/swift/Demangling/DemangleNodes.def",
33+
"include/swift/Demangling/ValueWitnessMangling.def",
34+
],
35+
linkerSettings: [
36+
.linkedLibrary("swift_Concurrency", .when(platforms: [.wasi])),
37+
]
38+
),
39+
],
40+
cxxLanguageStandard: .cxx14
1741
)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import JavaScriptKit
2+
3+
/// A `JSFunction` wrapper that enables async-function calls.
4+
/// Exceptions produced by JavaScript functions will be thrown as `JSValue`.
5+
///
6+
/// ```swift
7+
/// let fetch = JSObject.global.fetch.function!.async
8+
/// let result = await try! fetch("https://api.github.com/zen")
9+
/// ```
10+
public class JSAsyncFunction {
11+
private let base: JSFunction
12+
public init(_ base: JSFunction) {
13+
self.base = base
14+
}
15+
16+
/// Call this function with given `arguments` and binding given `this` as context.
17+
/// - Parameters:
18+
/// - this: The value to be passed as the `this` parameter to this function.
19+
/// - arguments: Arguments to be passed to this function.
20+
/// - Returns: The result of this call.
21+
@discardableResult
22+
public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) async throws -> JSValue {
23+
let result = base.callAsFunction(this: this, arguments: arguments)
24+
guard let object = result.object, let promise = JSPromise(object) else {
25+
fatalError("'\(result)' should be Promise object")
26+
}
27+
return await try promise.await()
28+
}
29+
30+
/// A variadic arguments version of `callAsFunction`.
31+
@discardableResult
32+
public func callAsFunction(this: JSObject? = nil, _ arguments: ConvertibleToJSValue...) async throws -> JSValue {
33+
await try callAsFunction(this: this, arguments: arguments)
34+
}
35+
}
36+
37+
public extension JSFunction {
38+
/// A modifier to call this function as a async function
39+
///
40+
/// ```swift
41+
/// let fetch = JSObject.global.fetch.function!.async
42+
/// let result = await try! fetch("https://api.github.com/zen")
43+
/// ```
44+
var `async`: JSAsyncFunction {
45+
JSAsyncFunction(self)
46+
}
47+
}
48+
49+
/// A `JSObject` wrapper that enables async method calls capturing `this`.
50+
/// Exceptions produced by JavaScript functions will be thrown as `JSValue`.
51+
@dynamicMemberLookup
52+
public class JSAsyncingObject {
53+
private let base: JSObject
54+
public init(_ base: JSObject) {
55+
self.base = base
56+
}
57+
58+
/// Returns the `name` member method binding this object as `this` context.
59+
/// - Parameter name: The name of this object's member to access.
60+
/// - Returns: The `name` member method binding this object as `this` context.
61+
public subscript(_ name: String) -> ((ConvertibleToJSValue...) async throws -> JSValue)? {
62+
guard let function = base[name].function?.async else { return nil }
63+
return { [base] (arguments: ConvertibleToJSValue...) in
64+
await try function(this: base, arguments: arguments)
65+
}
66+
}
67+
68+
/// A convenience method of `subscript(_ name: String) -> ((ConvertibleToJSValue...) throws -> JSValue)?`
69+
/// to access the member through Dynamic Member Lookup.
70+
public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) async throws -> JSValue)? {
71+
self[name]
72+
}
73+
}
74+
75+
76+
public extension JSObject {
77+
78+
/// A modifier to call methods as async methods capturing `this`
79+
///
80+
/// ```swift
81+
/// let fetch = JSObject.global.fetch.function!.async
82+
/// let result = await try! fetch("https://api.github.com/zen")
83+
/// ```
84+
var asyncing: JSAsyncingObject {
85+
JSAsyncingObject(self)
86+
}
87+
}

0 commit comments

Comments
 (0)