Skip to content

Proof of concept: Emscripten build for Luau and Lua 5.x #95

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
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
376 changes: 357 additions & 19 deletions build.zig
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 4 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -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",
},
},
}
6 changes: 1 addition & 5 deletions examples/interpreter.zig
Original file line number Diff line number Diff line change
@@ -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
4 changes: 1 addition & 3 deletions examples/zig-fn.zig
Original file line number Diff line number Diff line change
@@ -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
//
35 changes: 35 additions & 0 deletions src/emscripten/shell.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"/>
<title>Zig Lua</title>
<style>
body { margin: 0; background-color: black }
</style>
</head>
<body>
<script type='text/javascript'>
var Module = {
preRun: [],
postRun: [],
print: (function() {
return function(text) {
text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
};
})(),
printErr: function(text) {
text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
setStatus: function(text) { },
monitorRunDependencies: function(left) { },
};
window.onerror = function() {
console.log("onerror: " + event.message);
};
</script>
{{{ SCRIPT }}}
</body>
</html>
9 changes: 9 additions & 0 deletions src/emscripten/stdio.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#ifndef _STDIO_H_
#define _STDIO_H_

typedef struct FILE FILE;

// Conservative value - current Emscripten has 1024
#define BUFSIZ 512

#endif /* _STDIO_H_ */