Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental async/await support #112

Merged
merged 23 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattribute
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Sources/_CJavaScriptEventLoop/swift/* linguist-vendored
Sources/_CJavaScriptEventLoop/llvm/* linguist-vendored
2 changes: 1 addition & 1 deletion .swift-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
wasm-5.3.0-RELEASE
wasm-DEVELOPMENT-SNAPSHOT-2021-01-10-a
13 changes: 12 additions & 1 deletion Example/JavaScriptKitExample/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,16 @@ let package = Package(
),
],
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
targets: [.target(name: "JavaScriptKitExample", dependencies: ["JavaScriptKit"])]
targets: [
.target(
name: "JavaScriptKitExample",
dependencies: [
.product(name: "JavaScriptKit", package: "JavaScriptKit"),
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
],
swiftSettings: [
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
]
),
]
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import JavaScriptKit
import JavaScriptEventLoop

JavaScriptEventLoop.install()

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

_ = document.body.appendChild(buttonElement)

let fetch = JSObject.global.fetch.function!.async

func printZen() async {
let result = await try! fetch("https://api.github.com/zen").object!
let text = await try! result.asyncing.text!()
print(text)
}

JavaScriptEventLoop.runAsync {
await printZen()
}
2 changes: 2 additions & 0 deletions IntegrationTests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ benchmark: benchmark_setup run_benchmark
.PHONY: test
test: build_rt dist/PrimaryTests.wasm
node bin/primary-tests.js
concurrency_test: build_rt dist/ConcurrencyTests.wasm
node bin/concurrency-tests.js
9 changes: 9 additions & 0 deletions IntegrationTests/TestSuites/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ let package = Package(
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
targets: [
.target(name: "PrimaryTests", dependencies: ["JavaScriptKit"]),
.target(
name: "ConcurrencyTests",
dependencies: [
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
],
swiftSettings: [
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
]
),
.target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import JavaScriptKit

var printTestNames = false
// Uncomment the next line to print the name of each test suite before running it.
// This will make it easier to debug any errors that occur on the JS side.
//printTestNames = true

func test(_ name: String, testBlock: () throws -> Void) throws {
if printTestNames { print(name) }
do {
try testBlock()
} catch {
print("Error in \(name)")
print(error)
throw error
}
}

func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void {
if printTestNames { print(name) }
do {
await try testBlock()
} catch {
print("Error in \(name)")
print(error)
throw error
}
}

struct MessageError: Error {
let message: String
let file: StaticString
let line: UInt
let column: UInt
init(_ message: String, file: StaticString, line: UInt, column: UInt) {
self.message = message
self.file = file
self.line = line
self.column = column
}
}

func expectEqual<T: Equatable>(
_ lhs: T, _ rhs: T,
file: StaticString = #file, line: UInt = #line, column: UInt = #column
) throws {
if lhs != rhs {
throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column)
}
}

func expectCast<T, U>(
_ value: T, to type: U.Type = U.self,
file: StaticString = #file, line: UInt = #line, column: UInt = #column
) throws -> U {
guard let value = value as? U else {
throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column)
}
return value
}

func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject {
switch value {
case let .object(ref): return ref
default:
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
}
}

func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray {
guard let array = value.array else {
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
}
return array
}

func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction {
switch value {
case let .function(ref): return ref
default:
throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column)
}
}

func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool {
switch value {
case let .boolean(bool): return bool
default:
throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column)
}
}

func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double {
switch value {
case let .number(number): return number
default:
throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column)
}
}

func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String {
switch value {
case let .string(string): return String(string)
default:
throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column)
}
}

func expectAsyncThrow<T>(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error {
do {
_ = await try body()
} catch {
return error
}
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
}

func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws {
switch value {
case .some: return
case .none:
throw MessageError("Expect a non-nil value", file: file, line: line, column: column)
}
}
40 changes: 40 additions & 0 deletions IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import JavaScriptEventLoop
import JavaScriptKit

JavaScriptEventLoop.install()

try JavaScriptEventLoop.runAsync {
struct E: Error, Equatable {
let value: Int
}

await try asyncTest("Task.runDetached value") {
let handle = Task.runDetached { 1 }
await try expectEqual(handle.get(), 1)
}

await try asyncTest("Task.runDetached throws") {
let handle = Task.runDetached {
throw E(value: 2)
}
let error = await try expectAsyncThrow(await handle.get())
let e = try expectCast(error, to: E.self)
try expectEqual(e, E(value: 2))
}

await try asyncTest("await resolved Promise") {
let p = JSPromise(resolver: { resolve in
resolve(.success(1))
})
await try expectEqual(p.await(), 1)
}

await try asyncTest("await rejected Promise") {
let p = JSPromise(resolver: { resolve in
resolve(.failure(.number(3)))
})
let error = await try expectAsyncThrow(await p.await())
let jsValue = try expectCast(error, to: JSValue.self)
try expectEqual(jsValue, 3)
}
}
15 changes: 15 additions & 0 deletions IntegrationTests/bin/concurrency-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { startWasiTask } = require("../lib");

global.fetch = require('node-fetch');
global.sleep = function () {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}

startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => {
console.log(err);
process.exit(1);
});
28 changes: 26 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
// swift-tools-version:5.2
// swift-tools-version:5.3

import PackageDescription

let package = Package(
name: "JavaScriptKit",
products: [
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
],
targets: [
.target(
name: "JavaScriptKit",
dependencies: ["_CJavaScriptKit"]
),
.target(
name: "JavaScriptEventLoop",
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"],
swiftSettings: [
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
]
),
.target(name: "_CJavaScriptKit"),
]
.target(
name: "_CJavaScriptEventLoop",
dependencies: ["_CJavaScriptKit"],
exclude: [
"README", "LICENSE-llvm", "LICENSE-swift", "scripts",
"include/swift/ABI/MetadataKind.def",
"include/swift/ABI/ValueWitness.def",
"include/swift/AST/ReferenceStorage.def",
"include/swift/Demangling/DemangleNodes.def",
"include/swift/Demangling/ValueWitnessMangling.def",
],
linkerSettings: [
.linkedLibrary("swift_Concurrency", .when(platforms: [.wasi])),
]
),
],
cxxLanguageStandard: .cxx14
)
87 changes: 87 additions & 0 deletions Sources/JavaScriptEventLoop/JSAsyncFunction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import JavaScriptKit

/// A `JSFunction` wrapper that enables async-function calls.
/// Exceptions produced by JavaScript functions will be thrown as `JSValue`.
///
/// ```swift
/// let fetch = JSObject.global.fetch.function!.async
/// let result = await try! fetch("https://api.github.com/zen")
/// ```
public class JSAsyncFunction {
private let base: JSFunction
public init(_ base: JSFunction) {
self.base = base
}

/// 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: [ConvertibleToJSValue]) async throws -> JSValue {
let result = base.callAsFunction(this: this, arguments: arguments)
guard let object = result.object, let promise = JSPromise(object) else {
fatalError("'\(result)' should be Promise object")
}
return await try promise.await()
}

/// A variadic arguments version of `callAsFunction`.
@discardableResult
public func callAsFunction(this: JSObject? = nil, _ arguments: ConvertibleToJSValue...) async throws -> JSValue {
await try callAsFunction(this: this, arguments: arguments)
}
}

public extension JSFunction {
/// A modifier to call this function as a async function
///
/// ```swift
/// let fetch = JSObject.global.fetch.function!.async
/// let result = await try! fetch("https://api.github.com/zen")
/// ```
var `async`: JSAsyncFunction {
JSAsyncFunction(self)
}
}

/// A `JSObject` wrapper that enables async method calls capturing `this`.
/// Exceptions produced by JavaScript functions will be thrown as `JSValue`.
@dynamicMemberLookup
public class JSAsyncingObject {
private let base: JSObject
public init(_ base: JSObject) {
self.base = base
}

/// Returns the `name` member method binding this object as `this` context.
/// - Parameter name: The name of this object's member to access.
/// - Returns: The `name` member method binding this object as `this` context.
public subscript(_ name: String) -> ((ConvertibleToJSValue...) async throws -> JSValue)? {
kateinoigakukun marked this conversation as resolved.
Show resolved Hide resolved
guard let function = base[name].function?.async else { return nil }
return { [base] (arguments: ConvertibleToJSValue...) in
await try function(this: base, arguments: arguments)
}
}

/// A convenience method of `subscript(_ name: String) -> ((ConvertibleToJSValue...) throws -> JSValue)?`
/// to access the member through Dynamic Member Lookup.
public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) async throws -> JSValue)? {
self[name]
}
}


public extension JSObject {

/// A modifier to call methods as async methods capturing `this`
///
/// ```swift
/// let fetch = JSObject.global.fetch.function!.async
/// let result = await try! fetch("https://api.github.com/zen")
/// ```
var asyncing: JSAsyncingObject {
JSAsyncingObject(self)
}
}