diff --git a/.github/ISSUE_TEMPLATE/dev_zig_nomination.md b/.github/ISSUE_TEMPLATE/dev_zig_nomination.md index 2013e7ad..10dc05cd 100644 --- a/.github/ISSUE_TEMPLATE/dev_zig_nomination.md +++ b/.github/ISSUE_TEMPLATE/dev_zig_nomination.md @@ -44,7 +44,6 @@ You may have been linked to this issue because you sent a pull request to update These projects have zero `build.zig.zon` dependencies, we update them first - and in any order. -* [ ] mach-sysjs * [ ] mach-objc-generator * [ ] fastfilter * [ ] spirv-cross @@ -122,7 +121,6 @@ These projects have dependencies on other projects. We update them in the exact * mach-basisu * mach-freetype * font-assets - * mach-sysjs * mach-gpu * mach-glfw * mach-objc diff --git a/build.zig b/build.zig index bd7664d5..648c8813 100644 --- a/build.zig +++ b/build.zig @@ -78,13 +78,6 @@ pub fn build(b: *std.Build) !void { }); module.addImport("build-options", build_options.createModule()); - if ((want_mach or want_core or want_sysaudio) and target.result.cpu.arch == .wasm32) { - if (b.lazyDependency("mach_sysjs", .{ - .target = target, - .optimize = optimize, - })) |dep| module.addImport("mach-sysjs", dep.module("mach-sysjs")); - } - if (want_mach) { // Linux gamemode requires libc. if (target.result.os.tag == .linux) module.link_libc = true; @@ -434,7 +427,7 @@ pub const CoreApp = struct { } } if (platform == .web) { - inline for (.{ sdkPath("/src/core/platform/wasm/mach.js"), @import("mach_sysjs").getJSPath() }) |js| { + inline for (.{ sdkPath("/src/core/platform/wasm/mach.js"), sdkPath("/src/sysjs/mach-sysjs.js") }) |js| { const install_js = app_builder.addInstallFileWithDir( .{ .path = js }, std.Build.InstallDir{ .custom = "www" }, diff --git a/build.zig.zon b/build.zig.zon index 0adb06f4..27fd6a11 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -32,12 +32,6 @@ .hash = "12205a32c8e6ca23c68191b1e95405d2bd5f8e3055cba1c8ce0738d673ef49aef913", .lazy = true, }, - .mach_sysjs = .{ - .url = "https://pkg.machengine.org/mach-sysjs/eeef024f79beae189b7a4ed85e64ed076e76d538.tar.gz", - .hash = "1220db6845ce34743ae2a1ab0222efc942496adde2736c20e3443d4fde4ef64b11b9", - // TODO(build): be able to mark this dependency as lazy - // .lazy = true, - }, .mach_objc = .{ .url = "https://pkg.machengine.org/mach-objc/a7c3483702998aa0e960a788b9f611389f17d402.tar.gz", .hash = "1220049052fca861248fa6fb8bc24ecdb038049be0a3b22352bebc40e4dc2d2c981b", diff --git a/src/sysjs/mach-sysjs.js b/src/sysjs/mach-sysjs.js new file mode 100644 index 00000000..cabeac40 --- /dev/null +++ b/src/sysjs/mach-sysjs.js @@ -0,0 +1,366 @@ +let text_decoder = new TextDecoder(); +let text_encoder = new TextEncoder(); +let log_buf = ""; + +let uindex = 0; +let indices = []; +let values = []; +let value_map = {}; + +class MemoryBlock { + constructor(mem, offset = 0) { + this.mem = mem; + this.offset = offset; + } + + slice(offset) { + return new MemoryBlock(this.mem, offset); + } + + getMemory() { + return new DataView(this.mem, this.offset); + } + + getU8(offset) { + return this.getMemory().getUint8(offset, true); + } + + getU32(offset) { + return this.getMemory().getUint32(offset, true); + } + + getU64(offset) { + const ls = this.getU32(offset); + const ms = this.getU32(offset + 4); + + return ls + ms * 4294967296; + } + + getF64(offset) { + return this.getMemory().getFloat64(offset, true); + } + + getSlice(offset, len) { + return new Uint8Array(this.mem, offset, len); + } + + getString(offset, len) { + return text_decoder.decode(new Uint8Array(this.mem, offset, len)); + } + + setU8(offset, data) { + this.getMemory().setUint8(offset, data, true); + } + + setU32(offset, data) { + this.getMemory().setUint32(offset, data, true); + } + + setU64(offset, data) { + this.getMemory().setUint32(offset, data, true); + this.getMemory().setUint32(offset + 4, Math.floor(data / 4294967296), true); + } + + setF64(offset, data) { + this.getMemory().setFloat64(offset, data, true); + } + + setString(offset, str) { + const string = text_encoder.encode(str); + const buffer = new Uint8Array(this.mem, offset, string.length); + for (let i = 0; i < string.length; i += 1) { + buffer[i] = string[i]; + } + } +} + +const sysjs = { + wasm: undefined, + buffer: undefined, + + init(wasm) { + this.wasm = wasm; + + values = []; + value_map = {}; + this.addValue(globalThis); + }, + + addValue(value) { + value.__proto__.__uindex = uindex; + let idx = indices.pop(); + if (idx !== undefined) { + values[idx] = value; + } else { + idx = values.push(value) - 1; + } + value_map[uindex] = idx; + uindex += 1; + return idx; + }, + + zigCreateMap() { + return sysjs.addValue(new Map()); + }, + + zigCreateArray() { + return sysjs.addValue(new Array()); + }, + + zigCreateString(str, len) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + return sysjs.addValue(memory.getString(str, len)); + }, + + zigCreateFunction(id, captures, len) { + return sysjs.addValue(function () { + const args = sysjs.addValue(arguments); + sysjs.wasm.exports.wasmCallFunction(id, args, arguments.length, captures, len); + const return_value = values[args]["return_value"]; + sysjs.zigCleanupObject(args); + return return_value; + }); + }, + + getType(value) { + switch (typeof value) { + case "object": + switch (value) { + case null: + return 4; + default: + return 0; + } + case "number": + return 1; + case "boolean": + return 2; + case "string": + return 3; + case "undefined": + return 5; + case "function": + return 6; + } + }, + + writeObject(block, data, type) { + switch (type) { + case 0: + case 6: + block.setU8(0, type); + block.setU64(8, data); + break; + case 1: + block.setU8(0, 1); + block.setF64(8, data); + break; + case 2: + block.setU8(0, 2); + block.setU8(8, data); + break; + case 3: + block.setU8(0, 3); + block.setU64(8, data); + break; + case 4: + block.setU8(0, 4); + break; + case 5: + block.setU8(0, 5); + break; + } + }, + + readObject(block) { + switch (block.getU8(0)) { + case 0: + case 6: + return values[block.getU64(8)]; + case 1: + return block.getF64(8); + case 2: + return Boolean(block.getU8(8)); + case 3: + return values[block.getU64(8)]; + case 4: + return null; + case 5: + return undefined; + } + }, + + getPropertyEx(prop, ret_ptr, offset) { + let len = undefined; + const type = this.getType(prop); + switch (type) { + case 3: + len = prop.length; + case 0: + case 6: + if (prop in value_map) { + prop = value_map[prop.__uindex]; + } else { + prop = sysjs.addValue(prop); + } + break; + } + + if (len !== undefined) prop.__proto__.length = len; + + let memory = new MemoryBlock(ret_ptr, offset); + sysjs.writeObject(memory, prop, type); + }, + + getProperty(prop, ret_ptr) { + return sysjs.getPropertyEx(prop, sysjs.wasm.exports.memory.buffer, ret_ptr); + }, + + zigGetProperty(id, name, len, ret_ptr) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + let prop = values[id][memory.getString(name, len)]; + sysjs.getProperty(prop, ret_ptr); + }, + + zigSetProperty(id, name, len, set_ptr) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + values[id][memory.getString(name, len)] = sysjs.readObject( + memory.slice(set_ptr) + ); + }, + + zigDeleteProperty(id, name, len) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + delete values[id][memory.getString(name, len)]; + }, + + zigGetIndex(id, index, ret_ptr) { + let prop = values[id][index]; + sysjs.getProperty(prop, ret_ptr); + }, + + zigSetIndex(id, index, set_ptr) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + values[id][index] = sysjs.readObject(memory.slice(set_ptr)); + }, + + zigDeleteIndex(id, index) { + delete values[id][index]; + }, + + zigCopyBytes(id, bytes, expected_length) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + const array = values[id]; + if (array.length != expected_length) { + throw Error("copyBytes given array of length " + expected_length + " but destination has length " + array.length); + } + const slice = memory.getSlice(bytes, array.length); + array.set(slice); + }, + + zigGetAttributeCount(id) { + let obj = values[id]; + return Object.keys(obj).length; + }, + + zigCleanupObject(id) { + const idx = Number(id); + delete value_map[values[idx].__uindex]; + delete values[idx]; + indices.push(idx); + }, + + zigGetStringLength(val_id) { + return values[value_map[val_id]].length; + }, + + zigGetString(val_id, ptr) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + memory.setString(ptr, values[value_map[val_id]]); + }, + + zigValueEqual(val, other) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + const val_js = sysjs.readObject(memory.slice(val)); + const other_js = sysjs.readObject(memory.slice(other)); + return val_js === other_js; + }, + + zigValueInstanceOf(val, other) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + const val_js = sysjs.readObject(memory.slice(val)); + const other_js = sysjs.readObject(memory.slice(other)); + return val_js instanceof other_js; + }, + + functionCall(func, this_param, args, args_len, ret_ptr) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + let argv = []; + for (let i = 0; i < args_len; i += 1) { + argv.push(sysjs.readObject(memory.slice(args + i * 16))); + } + + let result = func.apply(this_param, argv); + + let length = undefined; + const type = sysjs.getType(result); + switch (type) { + case 3: + length = result.length; + case 0: + case 6: + result = sysjs.addValue(result); + break; + } + + if (length !== undefined) result.__proto__.length = length; + + sysjs.writeObject(memory.slice(ret_ptr), result, type); + }, + + zigFunctionCall(id, name, len, args, args_len, ret_ptr) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + sysjs.functionCall( + values[id][memory.getString(name, len)], + values[id], + args, + args_len, + ret_ptr + ); + }, + + zigFunctionInvoke(id, args, args_len, ret_ptr) { + sysjs.functionCall(values[id], undefined, args, args_len, ret_ptr); + }, + + zigGetParamCount(id) { + return values[id].length; + }, + + zigConstructType(id, args, args_len) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + let argv = []; + for (let i = 0; i < args_len; i += 1) { + argv.push(sysjs.readObject(memory.slice(args + i * 16))); + } + + return sysjs.addValue(new values[id](...argv)); + }, + + wzLogWrite(str, len) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + log_buf += memory.getString(str, len); + }, + + wzLogFlush() { + console.log(log_buf); + log_buf = ""; + }, + + wzPanic(str, len) { + let memory = new MemoryBlock(sysjs.wasm.exports.memory.buffer); + throw Error(memory.getString(str, len)); + }, +}; + +export { sysjs }; diff --git a/src/sysjs/main.zig b/src/sysjs/main.zig new file mode 100644 index 00000000..a0cdb9d7 --- /dev/null +++ b/src/sysjs/main.zig @@ -0,0 +1,269 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const builtin = @import("builtin"); + +const js = struct { + extern "sysjs" fn zigCreateMap() u32; + extern "sysjs" fn zigCreateArray() u32; + extern "sysjs" fn zigCreateString(str: [*]const u8, len: u32) u32; + extern "sysjs" fn zigCreateFunction(id: *const anyopaque, captures: [*]Value, len: u32) u32; + extern "sysjs" fn zigGetAttributeCount(id: u64) u32; + extern "sysjs" fn zigGetProperty(id: u64, name: [*]const u8, len: u32, ret_ptr: *anyopaque) void; + extern "sysjs" fn zigSetProperty(id: u64, name: [*]const u8, len: u32, set_ptr: *const anyopaque) void; + extern "sysjs" fn zigDeleteProperty(id: u64, name: [*]const u8, len: u32) void; + extern "sysjs" fn zigGetIndex(id: u64, index: u32, ret_ptr: *anyopaque) void; + extern "sysjs" fn zigSetIndex(id: u64, index: u32, set_ptr: *const anyopaque) void; + extern "sysjs" fn zigGetString(val_id: u64, ptr: [*]const u8) void; + extern "sysjs" fn zigGetStringLength(val_id: u64) u32; + extern "sysjs" fn zigValueEqual(val: *const anyopaque, other: *const anyopaque) bool; + extern "sysjs" fn zigValueInstanceOf(val: *const anyopaque, other: *const anyopaque) bool; + extern "sysjs" fn zigDeleteIndex(id: u64, index: u32) void; + extern "sysjs" fn zigCopyBytes(id: u64, bytes: [*]u8, expected_len: u32) void; + extern "sysjs" fn zigFunctionCall(id: u64, name: [*]const u8, len: u32, args: ?*const anyopaque, args_len: u32, ret_ptr: *anyopaque) void; + extern "sysjs" fn zigFunctionInvoke(id: u64, args: ?*const anyopaque, args_len: u32, ret_ptr: *anyopaque) void; + extern "sysjs" fn zigGetParamCount(id: u64) u32; + extern "sysjs" fn zigConstructType(id: u64, args: ?*const anyopaque, args_len: u32) u32; + extern "sysjs" fn zigCleanupObject(id: u64) void; +}; + +pub const Value = extern struct { + tag: Tag, + val: extern union { + ref: u64, + num: f64, + bool: bool, + }, + + pub const Tag = enum(u8) { + object, + num, + bool, + str, + null, + undefined, + func, + }; + + pub fn is(val: *const Value, comptime tag: Tag) bool { + return val.tag == tag; + } + + pub fn view(val: *const Value, comptime tag: Tag) switch (tag) { + .object => Object, + .num => f64, + .bool => bool, + .str => String, + .func => Function, + .null, .undefined => @compileError("Cannot get null or undefined as a value"), + } { + return switch (tag) { + .object => Object{ .ref = val.val.ref }, + .num => val.val.num, + .bool => val.val.bool, + .str => String{ .ref = val.val.ref }, + .func => Function{ .ref = val.val.ref }, + else => unreachable, + }; + } + + pub fn eql(val: *const Value, other: Value) bool { + if (val.tag != other.tag) + return false; + + return switch (val.tag) { + .num => val.val.num == other.val.num, + .bool => val.val.bool == other.val.bool, + // Using JS equality (===) is better here since lets say a ref can be dangling + else => js.zigValueEqual(val, &other), + }; + } + + pub fn instanceOf(val: *const Value, other: Value) bool { + return js.zigValueInstanceOf(val, &other); + } + + pub fn format(val: *const Value, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (val.tag) { + .object => try writer.print("Object@{}({})", .{ val.val.ref, val.view(.object).attributeCount() }), + .num => if (val.val.num > 10000) + try writer.print("{e}", .{val.val.num}) + else + try writer.print("{d}", .{val.val.num}), + .bool => try writer.writeAll(if (val.view(.bool)) "true" else "false"), + .str => try writer.print("String@{}({})", .{ val.val.ref, val.view(.str).getLength() }), + .func => try writer.print("Function@{}({})", .{ val.val.ref, val.view(.func).paramCount() }), + .null => try writer.writeAll("null"), + .undefined => try writer.writeAll("undefined"), + } + } +}; + +pub const Object = struct { + ref: u64, + + pub fn deinit(obj: *const Object) void { + js.zigCleanupObject(obj.ref); + } + + pub fn toValue(obj: *const Object) Value { + return .{ .tag = .object, .val = .{ .ref = obj.ref } }; + } + + pub fn attributeCount(obj: *const Object) usize { + return js.zigGetAttributeCount(obj.ref); + } + + pub fn get(obj: *const Object, prop: []const u8) Value { + var ret: Value = undefined; + js.zigGetProperty(obj.ref, prop.ptr, @as(u32, @intCast(prop.len)), &ret); + return ret; + } + + pub fn set(obj: *const Object, prop: []const u8, value: Value) void { + js.zigSetProperty(obj.ref, prop.ptr, @as(u32, @intCast(prop.len)), &value); + } + + pub fn delete(obj: *const Object, prop: []const u8) void { + js.zigDeleteProperty(obj.ref, prop.ptr, @as(u32, @intCast(prop.len))); + } + + pub fn getIndex(obj: *const Object, index: u32) Value { + var ret: Value = undefined; + js.zigGetIndex(obj.ref, index, &ret); + return ret; + } + + pub fn setIndex(obj: *const Object, index: u32, value: Value) void { + js.zigSetIndex(obj.ref, index, &value); + } + + pub fn deleteIndex(obj: *const Object, index: u32) void { + js.zigDeleteIndex(obj.ref, index); + } + + pub fn copyBytes(obj: *const Object, bytes: []u8) void { + js.zigCopyBytes(obj.ref, bytes.ptr, @as(u32, @intCast(bytes.len))); + } + + pub fn call(obj: *const Object, fun: []const u8, args: []const Value) Value { + var ret: Value = undefined; + js.zigFunctionCall(obj.ref, fun.ptr, @as(u32, @intCast(fun.len)), args.ptr, @as(u32, @intCast(args.len)), &ret); + return ret; + } +}; + +pub const Function = struct { + ref: u64, + + pub fn deinit(func: *const Function) void { + js.zigCleanupObject(func.ref); + } + + pub fn toValue(func: *const Function) Value { + return .{ .tag = .func, .val = .{ .ref = func.ref } }; + } + + pub fn paramCount(func: *const Function) usize { + // FIXME: native functions would always return 0 + return js.zigGetParamCount(func.ref); + } + + pub fn construct(func: *const Function, args: []const Value) Object { + return .{ .ref = js.zigConstructType(func.ref, args.ptr, @as(u32, @intCast(args.len))) }; + } + + pub fn invoke(func: *const Function, args: []const Value) Value { + var ret: Value = undefined; + js.zigFunctionInvoke(func.ref, args.ptr, @as(u32, @intCast(args.len)), &ret); + return ret; + } +}; + +pub const String = struct { + ref: u64, + + pub fn deinit(string: *const String) void { + js.zigCleanupObject(string.ref); + } + + pub fn toValue(string: *const String) Value { + return .{ .tag = .str, .val = .{ .ref = string.ref } }; + } + + pub fn getLength(string: *const String) usize { + return js.zigGetStringLength(string.ref); + } + + pub fn getOwnedSlice(string: *const String, allocator: std.mem.Allocator) ![]const u8 { + const slice = try allocator.alloc(u8, string.getLength()); + errdefer allocator.free(slice); + js.zigGetString(string.ref, slice.ptr); + return slice; + } +}; + +export fn wasmCallFunction(id: *anyopaque, args: u32, len: u32, captures: [*]Value, captures_len: u32) void { + var captures_slice: []Value = undefined; + captures_slice.ptr = captures; + captures_slice.len = captures_len; + + const obj = Object{ .ref = args }; + const func = @as(FunType, @ptrCast(@alignCast(id))); + obj.set("return_value", func(obj, len, captures_slice)); +} + +pub fn global() Object { + return Object{ .ref = 0 }; +} + +pub fn createMap() Object { + return .{ .ref = js.zigCreateMap() }; +} + +pub fn createArray() Object { + return .{ .ref = js.zigCreateArray() }; +} + +pub fn createString(string: []const u8) String { + return .{ .ref = js.zigCreateString(string.ptr, @as(u32, @intCast(string.len))) }; +} + +pub fn createNumber(num: anytype) Value { + switch (@typeInfo(@TypeOf(num))) { + .Int, + .Float, + .ComptimeInt, + .ComptimeFloat, + => return .{ .tag = .num, .val = .{ .num = std.math.lossyCast(f64, num) } }, + else => @compileError("expected integer, found " ++ @typeName(@TypeOf(num))), + } +} + +pub fn createBool(val: bool) Value { + return .{ .tag = .bool, .val = .{ .bool = val } }; +} + +pub fn createNull() Value { + return .{ .tag = .null, .val = undefined }; +} + +pub fn createUndefined() Value { + return .{ .tag = .undefined, .val = undefined }; +} + +const FunType = *const fn (args: Object, args_len: u32, captures: []Value) Value; + +pub fn createFunction(fun: FunType, captures: []Value) Function { + return .{ .ref = js.zigCreateFunction(fun, captures.ptr, @as(u32, @intCast(captures.len))) }; +} + +pub fn constructType(t: []const u8, args: []const Value) Object { + const constructor = global().get(t).view(.func); + defer constructor.deinit(); + + return constructor.construct(args); +} + +test { + std.testing.refAllDeclsRecursive(@This()); +}