diff --git a/build.zig b/build.zig index 08e024f..f752a4b 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const builtin = @import("builtin"); +const OptimizeMode = std.builtin.OptimizeMode; const Build = std.Build; const Step = std.Build.Step; @@ -15,9 +17,11 @@ pub const Language = enum { pub fn build(b: *Build) void { // Remove the default install and uninstall steps b.top_level_steps = .{}; + const emsdk = b.dependency("emsdk", .{}); const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const target_is_emscripten = target.result.os.tag == .emscripten; const lang = b.option(Language, "lang", "Lua language version to build") orelse .lua54; const shared = b.option(bool, "shared", "Build shared library instead of static") orelse false; @@ -45,10 +49,14 @@ pub fn build(b: *Build) void { const upstream = b.dependency(@tagName(lang), .{}); - const lib = switch (lang) { + const lib = if (!target_is_emscripten) switch (lang) { .luajit => buildLuaJIT(b, target, optimize, upstream, shared), .luau => buildLuau(b, target, optimize, upstream, luau_use_4_vector), else => buildLua(b, target, optimize, upstream, lang, shared), + } else switch (lang) { + .luajit => @panic("LuaJIT is not supported on Emscripten"), + .luau => buildLuauEmscripten(b, target, optimize, upstream, luau_use_4_vector, emsdk), + else => buildLuaEmscripten(b, target, optimize, upstream, lang, emsdk), }; // Expose the Lua artifact @@ -89,24 +97,59 @@ pub fn build(b: *Build) void { const examples = if (lang == .luau) &common_examples ++ luau_examples else &common_examples; for (examples) |example| { - const exe = b.addExecutable(.{ - .name = example[0], - .root_source_file = b.path(example[1]), - .target = target, - .optimize = optimize, - }); - exe.root_module.addImport("ziglua", ziglua); - - const artifact = b.addInstallArtifact(exe, .{}); - const exe_step = b.step(b.fmt("install-example-{s}", .{example[0]}), b.fmt("Install {s} example", .{example[0]})); - exe_step.dependOn(&artifact.step); - - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| run_cmd.addArgs(args); - - const run_step = b.step(b.fmt("run-example-{s}", .{example[0]}), b.fmt("Run {s} example", .{example[0]})); - run_step.dependOn(&run_cmd.step); + if (!target_is_emscripten) { + const exe = b.addExecutable(.{ + .name = example[0], + .root_source_file = b.path(example[1]), + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("ziglua", ziglua); + + const artifact = b.addInstallArtifact(exe, .{}); + const exe_step = b.step(b.fmt("install-example-{s}", .{example[0]}), b.fmt("Install {s} example", .{example[0]})); + exe_step.dependOn(&artifact.step); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + + const run_step = b.step(b.fmt("run-example-{s}", .{example[0]}), b.fmt("Run {s} example", .{example[0]})); + run_step.dependOn(&run_cmd.step); + } else { + const example_lib = b.addStaticLibrary(.{ + .name = example[0], + .root_source_file = b.path(example[1]), + .target = target, + .optimize = optimize, + }); + if (try emSdkSetupStep(b, emsdk)) |emsdk_setup| { + example_lib.step.dependOn(&emsdk_setup.step); + } + example_lib.linkLibC(); + example_lib.root_module.addImport("ziglua", ziglua); + + // create a special emcc linker run step + const link_step = try emLinkStep(b, .{ + .lib_main = example_lib, + .target = target, + .optimize = optimize, + .emsdk = emsdk, + .use_emmalloc = true, + .use_filesystem = true, + .shell_file_path = b.path("src/emscripten/shell.html"), + .extra_args = &.{ + "-sUSE_OFFSET_CONVERTER=1", + }, + }); + // ...and a special run step to run the build result via emrun + const run = emRunStep(b, .{ + .name = example[0], + .emsdk = emsdk, + }); + run.step.dependOn(&link_step.step); + b.step(b.fmt("run-example-{s}", .{example[0]}), b.fmt("Run {s} example", .{example[0]})).dependOn(&run.step); + } } const docs = b.addObject(.{ @@ -189,6 +232,84 @@ fn buildLua(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Optim return lib; } +fn buildLuaEmscripten( + b: *Build, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + upstream: *Build.Dependency, + lang: Language, + emsdk: *Build.Dependency, +) *Step.Compile { + const lib_opts = .{ + .name = "lua", + .target = target, + .optimize = optimize, + .version = switch (lang) { + .lua51 => std.SemanticVersion{ .major = 5, .minor = 1, .patch = 5 }, + .lua52 => std.SemanticVersion{ .major = 5, .minor = 2, .patch = 4 }, + .lua53 => std.SemanticVersion{ .major = 5, .minor = 3, .patch = 6 }, + .lua54 => std.SemanticVersion{ .major = 5, .minor = 4, .patch = 6 }, + else => unreachable, + }, + }; + const lib = b.addStaticLibrary(lib_opts); + if (try emSdkSetupStep(b, emsdk)) |emsdk_setup| { + lib.step.dependOn(&emsdk_setup.step); + } + + lib.addIncludePath(upstream.path("src")); + + const flags = [_][]const u8{ + // Standard version used in Lua Makefile + "-std=gnu99", + + // Define target-specific macro + switch (target.result.os.tag) { + .linux => "-DLUA_USE_LINUX", + .macos => "-DLUA_USE_MACOSX", + .windows => "-DLUA_USE_WINDOWS", + else => "-DLUA_USE_POSIX", + }, + + // Enable api check + if (optimize == .Debug) "-DLUA_USE_APICHECK" else "", + + "-I", + upstream.path("src").getPath(b), + }; + + const lua_source_files = switch (lang) { + .lua51 => &lua_base_source_files, + .lua52 => &lua_52_source_files, + .lua53 => &lua_53_source_files, + .lua54 => &lua_54_source_files, + else => unreachable, + }; + + for (lua_source_files) |file| { + const compile_lua = emCompileStep( + b, + upstream.path(file), + optimize, + emsdk, + &flags, + ); + lib.addObjectFile(compile_lua); + } + + lib.linkLibC(); + + // unsure why this is necessary, but even with linkLibC() lauxlib.h will fail to find stdio.h + lib.installHeader(b.path("src/emscripten/stdio.h"), "stdio.h"); + + lib.installHeader(upstream.path("src/lua.h"), "lua.h"); + lib.installHeader(upstream.path("src/lualib.h"), "lualib.h"); + lib.installHeader(upstream.path("src/lauxlib.h"), "lauxlib.h"); + lib.installHeader(upstream.path("src/luaconf.h"), "luaconf.h"); + + return lib; +} + /// Luau has diverged enough from Lua (C++, project structure, ...) that it is easier to separate the build logic fn buildLuau(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, upstream: *Build.Dependency, luau_use_4_vector: bool) *Step.Compile { const lib = b.addStaticLibrary(.{ @@ -230,6 +351,61 @@ fn buildLuau(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Opti return lib; } +fn buildLuauEmscripten( + b: *Build, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + upstream: *Build.Dependency, + luau_use_4_vector: bool, + emsdk: *Build.Dependency, +) *Step.Compile { + const lib = b.addStaticLibrary(.{ + .name = "luau", + .target = target, + .optimize = optimize, + .version = std.SemanticVersion{ .major = 0, .minor = 607, .patch = 0 }, + .link_libc = true, + }); + if (try emSdkSetupStep(b, emsdk)) |emsdk_setup| { + lib.step.dependOn(&emsdk_setup.step); + } + + const flags = .{ + "-DLUA_USE_LONGJMP=1", + "-DLUA_API=extern\"C\"", + "-DLUACODE_API=extern\"C\"", + "-DLUACODEGEN_API=extern\"C\"", + if (luau_use_4_vector) "-DLUA_VECTOR_SIZE=4" else "-DLUA_VECTOR_SIZE=3", + "-I", + upstream.path("Common/include").getPath(b), + "-I", + upstream.path("Compiler/include").getPath(b), + "-I", + upstream.path("Ast/include").getPath(b), + "-I", + upstream.path("VM/include").getPath(b), + }; + for (luau_source_files) |file| { + const compile_luau = emCompileStep( + b, + upstream.path(file), + optimize, + emsdk, + &flags, + ); + lib.addObjectFile(compile_luau); + } + lib.addObjectFile(emCompileStep( + b, + b.path("src/luau.cpp"), + optimize, + emsdk, + &flags, + )); + + return lib; +} + fn buildLuaJIT(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, upstream: *Build.Dependency, shared: bool) *Step.Compile { // TODO: extract this to the main build function because it is shared between all specialized build functions const lib_opts = .{ @@ -596,3 +772,165 @@ const luau_source_files = [_][]const u8{ "Ast/src/StringUtils.cpp", "Ast/src/TimeTrace.cpp", }; + +// for wasm32-emscripten, need to run the Emscripten linker from the Emscripten SDK +// NOTE: ideally this would go into a separate emsdk-zig package +pub const EmLinkOptions = struct { + target: Build.ResolvedTarget, + optimize: OptimizeMode, + lib_main: *Build.Step.Compile, // the actual Zig code must be compiled to a static link library + emsdk: *Build.Dependency, + release_use_closure: bool = true, + release_use_lto: bool = true, + use_emmalloc: bool = false, + use_filesystem: bool = true, + shell_file_path: ?Build.LazyPath, + extra_args: []const []const u8 = &.{}, +}; + +pub fn emCompileStep(b: *Build, filename: Build.LazyPath, optimize: OptimizeMode, emsdk: *Build.Dependency, extra_flags: []const []const u8) Build.LazyPath { + const emcc_path = emSdkLazyPath(b, emsdk, &.{ "upstream", "emscripten", "emcc" }).getPath(b); + const emcc = b.addSystemCommand(&.{emcc_path}); + emcc.setName("emcc"); // hide emcc path + emcc.addArg("-c"); + if (optimize == .ReleaseSmall) { + emcc.addArg("-Oz"); + } else if (optimize == .ReleaseFast or optimize == .ReleaseSafe) { + emcc.addArg("-O3"); + } + emcc.addFileArg(filename); + for (extra_flags) |flag| { + emcc.addArg(flag); + } + emcc.addArg("-o"); + + const output_name = switch (filename) { + .dependency => filename.dependency.sub_path, + .src_path => filename.src_path.sub_path, + .cwd_relative => filename.cwd_relative, + .generated => filename.generated.sub_path, + }; + + const output = emcc.addOutputFileArg(b.fmt("{s}.o", .{output_name})); + return output; +} + +pub fn emLinkStep(b: *Build, options: EmLinkOptions) !*Build.Step.InstallDir { + const emcc_path = emSdkLazyPath(b, options.emsdk, &.{ "upstream", "emscripten", "emcc" }).getPath(b); + const emcc = b.addSystemCommand(&.{emcc_path}); + emcc.setName("emcc"); // hide emcc path + if (options.optimize == .Debug) { + emcc.addArgs(&.{ "-Og", "-sSAFE_HEAP=1", "-sSTACK_OVERFLOW_CHECK=1" }); + } else { + emcc.addArg("-sASSERTIONS=0"); + if (options.optimize == .ReleaseSmall) { + emcc.addArg("-Oz"); + } else { + emcc.addArg("-O3"); + } + if (options.release_use_lto) { + emcc.addArg("-flto"); + } + if (options.release_use_closure) { + emcc.addArgs(&.{ "--closure", "1" }); + } + } + if (!options.use_filesystem) { + emcc.addArg("-sNO_FILESYSTEM=1"); + } + if (options.use_emmalloc) { + emcc.addArg("-sMALLOC='emmalloc'"); + } + if (options.shell_file_path) |shell_file_path| { + emcc.addPrefixedFileArg("--shell-file=", shell_file_path); + } + for (options.extra_args) |arg| { + emcc.addArg(arg); + } + + // add the main lib, and then scan for library dependencies and add those too + emcc.addArtifactArg(options.lib_main); + var it = options.lib_main.root_module.iterateDependencies(options.lib_main, false); + while (it.next()) |item| { + for (item.module.link_objects.items) |link_object| { + switch (link_object) { + .other_step => |compile_step| { + switch (compile_step.kind) { + .lib => { + emcc.addArtifactArg(compile_step); + }, + else => {}, + } + }, + else => {}, + } + } + } + emcc.addArg("-o"); + const out_file = emcc.addOutputFileArg(b.fmt("{s}.html", .{options.lib_main.name})); + + // the emcc linker creates 3 output files (.html, .wasm and .js) + const install = b.addInstallDirectory(.{ + .source_dir = out_file.dirname(), + .install_dir = .prefix, + .install_subdir = "web", + }); + install.step.dependOn(&emcc.step); + + // get the emcc step to run on 'zig build' + b.getInstallStep().dependOn(&install.step); + return install; +} + +// build a run step which uses the emsdk emrun command to run a build target in the browser +// NOTE: ideally this would go into a separate emsdk-zig package +pub const EmRunOptions = struct { + name: []const u8, + emsdk: *Build.Dependency, +}; +pub fn emRunStep(b: *Build, options: EmRunOptions) *Build.Step.Run { + const emrun_path = b.findProgram(&.{"emrun"}, &.{}) catch emSdkLazyPath(b, options.emsdk, &.{ "upstream", "emscripten", "emrun" }).getPath(b); + const emrun = b.addSystemCommand(&.{ emrun_path, b.fmt("{s}/web/{s}.html", .{ b.install_path, options.name }) }); + return emrun; +} + +// helper function to build a LazyPath from the emsdk root and provided path components +fn emSdkLazyPath(b: *Build, emsdk: *Build.Dependency, subPaths: []const []const u8) Build.LazyPath { + return emsdk.path(b.pathJoin(subPaths)); +} + +fn createEmsdkStep(b: *Build, emsdk: *Build.Dependency) *Build.Step.Run { + if (builtin.os.tag == .windows) { + return b.addSystemCommand(&.{emSdkLazyPath(b, emsdk, &.{"emsdk.bat"}).getPath(b)}); + } else { + const step = b.addSystemCommand(&.{"bash"}); + step.addArg(emSdkLazyPath(b, emsdk, &.{"emsdk"}).getPath(b)); + return step; + } +} + +// One-time setup of the Emscripten SDK (runs 'emsdk install + activate'). If the +// SDK had to be setup, a run step will be returned which should be added +// as dependency to the sokol library (since this needs the emsdk in place), +// if the emsdk was already setup, null will be returned. +// NOTE: ideally this would go into a separate emsdk-zig package +// NOTE 2: the file exists check is a bit hacky, it would be cleaner +// to build an on-the-fly helper tool which takes care of the SDK +// setup and just does nothing if it already happened +// NOTE 3: this code works just fine when the SDK version is updated in build.zig.zon +// since this will be cloned into a new zig cache directory which doesn't have +// an .emscripten file yet until the one-time setup. +fn emSdkSetupStep(b: *Build, emsdk: *Build.Dependency) !?*Build.Step.Run { + const dot_emsc_path = emSdkLazyPath(b, emsdk, &.{".emscripten"}).getPath(b); + const dot_emsc_exists = !std.meta.isError(std.fs.accessAbsolute(dot_emsc_path, .{})); + if (!dot_emsc_exists) { + const emsdk_install = createEmsdkStep(b, emsdk); + emsdk_install.addArgs(&.{ "install", "latest" }); + const emsdk_activate = createEmsdkStep(b, emsdk); + emsdk_activate.addArgs(&.{ "activate", "latest" }); + emsdk_activate.step.dependOn(&emsdk_install.step); + return emsdk_activate; + } else { + return null; + } +} diff --git a/build.zig.zon b/build.zig.zon index 1f7fc38..caa79fd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,5 +37,9 @@ .url = "https://github.com/luau-lang/luau/archive/refs/tags/0.607.tar.gz", .hash = "122003818ff2aa912db37d4bbda314ff9ff70d03d9243af4b639490be98e2bfa7cb6", }, + .emsdk = .{ + .url = "git+https://github.com/emscripten-core/emsdk#3.1.61", + .hash = "12200ba39d83227f5de08287b043b011a2eb855cdb077f4b165edce30564ba73400e", + }, }, } diff --git a/examples/interpreter.zig b/examples/interpreter.zig index effc62d..8396939 100644 --- a/examples/interpreter.zig +++ b/examples/interpreter.zig @@ -7,14 +7,10 @@ const std = @import("std"); const ziglua = @import("ziglua"); pub fn main() anyerror!void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - defer _ = gpa.deinit(); - // Initialize The Lua vm and get a reference to the main thread // // Passing a Zig allocator to the Lua state requires a stable pointer - var lua = try ziglua.Lua.init(&allocator); + var lua = try ziglua.Lua.init(&std.heap.c_allocator); defer lua.deinit(); // Open all Lua standard libraries diff --git a/examples/zig-fn.zig b/examples/zig-fn.zig index 203fd6b..cce903a 100644 --- a/examples/zig-fn.zig +++ b/examples/zig-fn.zig @@ -17,9 +17,7 @@ fn adder(lua: *Lua) i32 { } pub fn main() anyerror!void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - defer _ = gpa.deinit(); + const allocator = std.heap.c_allocator; // Initialize The Lua vm and get a reference to the main thread // diff --git a/src/emscripten/shell.html b/src/emscripten/shell.html new file mode 100644 index 0000000..cee18c3 --- /dev/null +++ b/src/emscripten/shell.html @@ -0,0 +1,35 @@ + + +
+ + +