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

Add support for BigInts and BigInt-based TypedArrays #184

Merged
merged 35 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fa93b91
Initial BigInt support
j-f1 Apr 9, 2022
3ca1539
Switch to const enums
j-f1 Apr 9, 2022
bf3bac3
Add JSBigInt/clamped method, fix overload
j-f1 Apr 9, 2022
8c02c79
Convert BigInts into Swift ints automatically
j-f1 Apr 9, 2022
814774b
Add support for JAVASCRIPTKIT_WITHOUT_BIGINTS
j-f1 Apr 9, 2022
0bcb359
Allow Int64/UInt64-based arrays
j-f1 Apr 9, 2022
d1543fc
Don’t use BigInt literal for backwards compat
j-f1 Apr 9, 2022
f8838eb
bump lib version
j-f1 Apr 9, 2022
26b51c7
long long?
j-f1 Apr 9, 2022
4dd4aae
document JAVASCRIPTKIT_WITHOUT_BIGINTS flag
j-f1 Apr 9, 2022
cd3c076
check compatibility workflow builds all variants
j-f1 Apr 9, 2022
76303b9
Increase error stack trace limit
j-f1 Apr 9, 2022
c356aba
Use correct type for setTimeout in JavaScriptEventLoop
j-f1 Apr 9, 2022
6f150ad
Add symbol support to runtime’s JSValue.decode
j-f1 Apr 9, 2022
6257d35
remove JavaScriptValueKindInvalid since neither side produces it
j-f1 Apr 9, 2022
203844f
use assertNever to ensure runtime switch statements are kept up to date
j-f1 Apr 9, 2022
9d066f9
BigInt tests
j-f1 Apr 9, 2022
1fdcd26
Revert changes to README
j-f1 Apr 10, 2022
14541b8
consistent whitespace (by semantic grouping) in index.ts
j-f1 Apr 10, 2022
dcd38c7
Rename: type→kind
j-f1 Apr 10, 2022
63f33d0
drop implicit return
j-f1 Apr 10, 2022
30a3e66
assert Int/UInt types are 32-bit for now
j-f1 Apr 10, 2022
68cb689
Require BigInts for ConvertibleToJSValue conformance on Int64/UInt64
j-f1 Apr 10, 2022
01ae32d
run Prettier on primary-tests.js
j-f1 Apr 10, 2022
a44de1a
move stackTraceLimit change to non-benchmark tests only
j-f1 Apr 10, 2022
6517b7e
remove JAVASCRIPTKIT_WITHOUT_BIGINTS
j-f1 Apr 16, 2022
c54bd10
SPI for JSObject_id
j-f1 Apr 16, 2022
7af5917
Move i64 stuff to a separate module
j-f1 Apr 16, 2022
b7a6a7d
fix Signed/UnsignedInteger ConstructibleFromJSValue
j-f1 Apr 16, 2022
b83611f
fix tests?
j-f1 Apr 16, 2022
4ca7705
Address review comments
j-f1 Apr 16, 2022
35dd9f7
Simplify JSValue: CustomStringConvertible conformance
j-f1 Apr 16, 2022
0ec8fc3
rename to JavaScriptBigIntSupport
j-f1 Apr 22, 2022
7960198
Formatting tweak
j-f1 Apr 22, 2022
5cff142
fix typo
j-f1 Apr 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ jobs:
make bootstrap
cd Example/JavaScriptKitExample
swift build --triple wasm32-unknown-wasi
swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS
swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_BIGINTS
swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS -Xswiftc -DJAVASCRIPTKIT_WITHOUT_BIGINTS
31 changes: 31 additions & 0 deletions IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -853,4 +853,35 @@ try test("JSValueDecoder") {
}


try test("BigInt") {
func expectPassesThrough(signed value: Int64) throws {
let bigInt = JSBigInt(value)
try expectEqual(bigInt.description, value.description)
}

func expectPassesThrough(unsigned value: UInt64) throws {
let bigInt = JSBigInt(unsigned: value)
try expectEqual(bigInt.description, value.description)
}

try expectPassesThrough(signed: 0)
try expectPassesThrough(signed: 1 << 62)
try expectPassesThrough(signed: -2305)
for _ in 0 ..< 100 {
try expectPassesThrough(signed: .random(in: .min ... .max))
}
try expectPassesThrough(signed: .min)
try expectPassesThrough(signed: .max)

try expectPassesThrough(unsigned: 0)
try expectPassesThrough(unsigned: 1 << 62)
try expectPassesThrough(unsigned: 1 << 63)
try expectPassesThrough(unsigned: .min)
try expectPassesThrough(unsigned: .max)
try expectPassesThrough(unsigned: ~0)
for _ in 0 ..< 100 {
try expectPassesThrough(unsigned: .random(in: .min ... .max))
}
}

Expectation.wait(expectations)
2 changes: 2 additions & 0 deletions IntegrationTests/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const promisify = require("util").promisify;
const fs = require("fs");
const readFile = promisify(fs.readFile);

Error.stackTraceLimit = Infinity;

const startWasiTask = async (wasmPath) => {
// Instantiate a new WASI Instance
const wasmFs = new WasmFs();
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ JavaScript features should work, which currently includes:
- Mobile Safari 14.8+

If you need to support older browser versions, you'll have to build with
`JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags
`JAVASCRIPTKIT_WITHOUT_WEAKREFS` and `JAVASCRIPTKIT_WITHOUT_BIGINTS` flags, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS -Xswiftc -DJAVASCRIPTKIT_WITHOUT_BIGINTS` flags
when compiling. This should lower browser requirements to these versions:

- Edge 16+
Expand Down
40 changes: 33 additions & 7 deletions Runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class SwiftRuntime {
private _instance: WebAssembly.Instance | null;
private _memory: Memory | null;
private _closureDeallocator: SwiftClosureDeallocator | null;
private version: number = 706;
private version: number = 707;

private textDecoder = new TextDecoder("utf-8");
private textEncoder = new TextEncoder(); // Only support utf-8
Expand All @@ -35,6 +35,17 @@ export class SwiftRuntime {
}`
);
}

if (this.exports.swjs_library_features() & LibraryFeatures.BigInts) {
if (typeof BigInt === "undefined") {
throw new Error(
"The Swift part of JavaScriptKit was configured to require " +
"the availability of JavaScript BigInts. Please build " +
"with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_BIGINTS` to " +
"disable features that use BigInts."
);
}
}
}

private get instance() {
Expand Down Expand Up @@ -114,7 +125,6 @@ export class SwiftRuntime {
const value = JSValue.decode(kind, payload1, payload2, this.memory);
obj[key] = value;
},

swjs_get_prop: (
ref: ref,
name: ref,
Expand Down Expand Up @@ -146,7 +156,6 @@ export class SwiftRuntime {
const value = JSValue.decode(kind, payload1, payload2, this.memory);
obj[index] = value;
},

swjs_get_subscript: (
ref: ref,
index: number,
Expand All @@ -172,15 +181,13 @@ export class SwiftRuntime {
this.memory.writeUint32(bytes_ptr_result, bytes_ptr);
return bytes.length;
},

swjs_decode_string: (bytes_ptr: pointer, length: number) => {
const bytes = this.memory
.bytes()
.subarray(bytes_ptr, bytes_ptr + length);
const string = this.textDecoder.decode(bytes);
return this.memory.retain(string);
},

swjs_load_string: (ref: ref, buffer: pointer) => {
const bytes = this.memory.getObject(ref);
this.memory.writeBytes(buffer, bytes);
Expand Down Expand Up @@ -290,7 +297,6 @@ export class SwiftRuntime {
this.memory
);
},

swjs_call_function_with_this_no_catch: (
obj_ref: ref,
func_ref: ref,
Expand Down Expand Up @@ -328,13 +334,13 @@ export class SwiftRuntime {
}
}
},

swjs_call_new: (ref: ref, argv: pointer, argc: number) => {
const constructor = this.memory.getObject(ref);
const args = JSValue.decodeArray(argv, argc, this.memory);
const instance = new constructor(...args);
return this.memory.retain(instance);
},

swjs_call_throwing_new: (
ref: ref,
argv: pointer,
Expand Down Expand Up @@ -409,5 +415,25 @@ export class SwiftRuntime {
swjs_release: (ref: ref) => {
this.memory.release(ref);
},

swjs_i64_to_bigint: (value: bigint, signed: number) => {
return this.memory.retain(
signed ? value : BigInt.asUintN(64, value)
);
},
swjs_bigint_to_i64: (ref: ref, signed: number) => {
const object = this.memory.getObject(ref);
if (typeof object !== "bigint") {
throw new Error(`Expected a BigInt, but got ${typeof object}`);
}
if (signed) {
return object;
} else {
if (object < BigInt(0)) {
return BigInt(0);
}
return BigInt.asIntN(64, object);
}
},
};
}
37 changes: 23 additions & 14 deletions Runtime/src/js-value.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Memory } from "./memory";
import { pointer } from "./types";
import { assertNever, pointer } from "./types";

export enum Kind {
Invalid = -1,
export const enum Kind {
Boolean = 0,
String = 1,
Number = 2,
Expand All @@ -11,6 +10,7 @@ export enum Kind {
Undefined = 5,
Function = 6,
Symbol = 7,
BigInt = 8,
}

export const decode = (
Expand All @@ -33,6 +33,8 @@ export const decode = (
case Kind.String:
case Kind.Object:
case Kind.Function:
case Kind.Symbol:
case Kind.BigInt:
return memory.getObject(payload1);

case Kind.Null:
Expand All @@ -42,7 +44,7 @@ export const decode = (
return undefined;

default:
throw new Error(`JSValue Type kind "${kind}" is not supported`);
assertNever(kind, `JSValue Type kind "${kind}" is not supported`);
}
};

Expand Down Expand Up @@ -73,7 +75,14 @@ export const write = (
memory.writeUint32(kind_ptr, exceptionBit | Kind.Null);
return;
}
switch (typeof value) {

const writeRef = (kind: Kind) => {
memory.writeUint32(kind_ptr, exceptionBit | kind);
memory.writeUint32(payload1_ptr, memory.retain(value));
};

const type = typeof value;
switch (type) {
case "boolean": {
memory.writeUint32(kind_ptr, exceptionBit | Kind.Boolean);
memory.writeUint32(payload1_ptr, value ? 1 : 0);
Expand All @@ -85,30 +94,30 @@ export const write = (
break;
}
case "string": {
memory.writeUint32(kind_ptr, exceptionBit | Kind.String);
memory.writeUint32(payload1_ptr, memory.retain(value));
writeRef(Kind.String);
break;
}
case "undefined": {
memory.writeUint32(kind_ptr, exceptionBit | Kind.Undefined);
break;
}
case "object": {
memory.writeUint32(kind_ptr, exceptionBit | Kind.Object);
memory.writeUint32(payload1_ptr, memory.retain(value));
writeRef(Kind.Object);
break;
}
case "function": {
memory.writeUint32(kind_ptr, exceptionBit | Kind.Function);
memory.writeUint32(payload1_ptr, memory.retain(value));
writeRef(Kind.Function);
break;
}
case "symbol": {
memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol);
memory.writeUint32(payload1_ptr, memory.retain(value));
writeRef(Kind.Symbol);
break;
}
case "bigint": {
writeRef(Kind.BigInt);
break;
}
default:
throw new Error(`Type "${typeof value}" is not supported yet`);
assertNever(type, `Type "${type}" is not supported yet`);
}
};
9 changes: 8 additions & 1 deletion Runtime/src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ export class Memory {
bytes = () => new Uint8Array(this.rawMemory.buffer);
dataView = () => new DataView(this.rawMemory.buffer);

writeBytes = (ptr: pointer, bytes: Uint8Array) => this.bytes().set(bytes, ptr);
writeBytes = (ptr: pointer, bytes: Uint8Array) =>
this.bytes().set(bytes, ptr);

readUint32 = (ptr: pointer) => this.dataView().getUint32(ptr, true);
readUint64 = (ptr: pointer) => this.dataView().getBigUint64(ptr, true);
readInt64 = (ptr: pointer) => this.dataView().getBigInt64(ptr, true);
readFloat64 = (ptr: pointer) => this.dataView().getFloat64(ptr, true);

writeUint32 = (ptr: pointer, value: number) =>
this.dataView().setUint32(ptr, value, true);
writeUint64 = (ptr: pointer, value: bigint) =>
this.dataView().setBigUint64(ptr, value, true);
writeInt64 = (ptr: pointer, value: bigint) =>
this.dataView().setBigInt64(ptr, value, true);
writeFloat64 = (ptr: pointer, value: number) =>
this.dataView().setFloat64(ptr, value, true);
}
10 changes: 9 additions & 1 deletion Runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as JSValue from "./js-value";

export type ref = number;
export type pointer = number;
export type bool = number;

export interface ExportedFunctions {
swjs_library_version(): number;
Expand Down Expand Up @@ -102,10 +103,13 @@ export interface ImportedFunctions {
): number;
swjs_load_typed_array(ref: ref, buffer: pointer): void;
swjs_release(ref: number): void;
swjs_i64_to_bigint(value: bigint, signed: bool): ref;
swjs_bigint_to_i64(ref: ref, signed: bool): bigint;
}

export enum LibraryFeatures {
export const enum LibraryFeatures {
WeakRefs = 1 << 0,
BigInts = 1 << 1,
}

export type TypedArray =
Expand All @@ -120,3 +124,7 @@ export type TypedArray =
// | BigUint64ArrayConstructor
| Float32ArrayConstructor
| Float64ArrayConstructor;

export function assertNever(x: never, message: string) {
throw new Error(message);
}
2 changes: 1 addition & 1 deletion Runtime/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"rootDir": "src",
"strict": true,
"target": "es2017",
"lib": ["es2017", "DOM", "ESNext.WeakRef"],
"lib": ["es2020", "DOM", "ESNext.WeakRef"],
"skipLibCheck": true
},
"include": ["src/**/*"],
Expand Down
6 changes: 3 additions & 3 deletions Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
/// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
let queueMicrotask: @Sendable (@escaping () -> Void) -> Void
/// A function that invokes a given closure after a specified number of milliseconds.
let setTimeout: @Sendable (UInt64, @escaping () -> Void) -> Void
let setTimeout: @Sendable (Double, @escaping () -> Void) -> Void

/// A mutable state to manage internal job queue
/// Note that this should be guarded atomically when supporting multi-threaded environment.
var queueState = QueueState()

private init(
queueTask: @Sendable @escaping (@escaping () -> Void) -> Void,
setTimeout: @Sendable @escaping (UInt64, @escaping () -> Void) -> Void
setTimeout: @Sendable @escaping (Double, @escaping () -> Void) -> Void
) {
self.queueMicrotask = queueTask
self.setTimeout = setTimeout
Expand Down Expand Up @@ -83,7 +83,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {

private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) {
let milliseconds = nanoseconds / 1_000_000
setTimeout(milliseconds, {
setTimeout(Double(milliseconds), {
job._runSynchronously(on: self.asUnownedSerialExecutor())
})
}
Expand Down
16 changes: 8 additions & 8 deletions Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,14 @@ extension UInt32: TypedArrayElement {
public static var typedArrayClass = JSObject.global.Uint32Array.function!
}

// FIXME: Support passing BigInts across the bridge
// See https://github.com/swiftwasm/JavaScriptKit/issues/56
//extension Int64: TypedArrayElement {
// public static var typedArrayClass = JSObject.global.BigInt64Array.function!
//}
//extension UInt64: TypedArrayElement {
// public static var typedArrayClass = JSObject.global.BigUint64Array.function!
//}
#if !JAVASCRIPTKIT_WITHOUT_BIGINTS
extension Int64: TypedArrayElement {
public static var typedArrayClass = JSObject.global.BigInt64Array.function!
}
extension UInt64: TypedArrayElement {
public static var typedArrayClass = JSObject.global.BigUint64Array.function!
}
#endif

extension Float32: TypedArrayElement {
public static var typedArrayClass = JSObject.global.Float32Array.function!
Expand Down
Loading