Skip to content

Commit 6b5d063

Browse files
committed
Expose Luau bytecode compiling and loading API
- Add ziglua.compile for a safer wrapper around c.luau_compile - Add Lua.loadBytecode() - Add example
1 parent e6f7d2f commit 6b5d063

File tree

5 files changed

+123
-2
lines changed

5 files changed

+123
-2
lines changed

build.zig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,14 @@ pub fn build(b: *Build) void {
6969
test_step.dependOn(&run_tests.step);
7070

7171
// Examples
72-
const examples = [_]struct { []const u8, []const u8 }{
72+
var common_examples = [_]struct { []const u8, []const u8 }{
7373
.{ "interpreter", "examples/interpreter.zig" },
7474
.{ "zig-function", "examples/zig-fn.zig" },
7575
};
76+
const luau_examples = [_]struct { []const u8, []const u8 }{
77+
.{ "luau-bytecode", "examples/luau-bytecode.zig" },
78+
};
79+
const examples = if (lang == .luau) &common_examples ++ luau_examples else &common_examples;
7680

7781
for (examples) |example| {
7882
const exe = b.addExecutable(.{

examples/luau-bytecode.zig

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//! Run Luau bytecode
2+
3+
// How to recompile `test.luau.bin` bytecode binary:
4+
//
5+
// luau-compile --binary test.luau > test.bc
6+
//
7+
// This may be required if the Luau version gets upgraded.
8+
9+
const std = @import("std");
10+
11+
// The ziglua module is made available in build.zig
12+
const ziglua = @import("ziglua");
13+
14+
pub fn main() anyerror!void {
15+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
16+
const allocator = gpa.allocator();
17+
defer _ = gpa.deinit();
18+
19+
// Initialize The Lua vm and get a reference to the main thread
20+
var lua = try ziglua.Lua.init(allocator);
21+
defer lua.deinit();
22+
23+
// Open all Lua standard libraries
24+
lua.openLibs();
25+
26+
// Load bytecode
27+
const src = @embedFile("./test.luau");
28+
const bc = try ziglua.compile(allocator, src, ziglua.CompileOptions{});
29+
defer allocator.free(bc);
30+
31+
try lua.loadBytecode("...", bc);
32+
try lua.protectedCall(0, 0, 0);
33+
}

examples/test.luau

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--!strict
2+
3+
function ispositive(x : number) : string
4+
if x > 0 then
5+
return "yes"
6+
else
7+
return "no"
8+
end
9+
end
10+
11+
local result : string
12+
result = ispositive(1)
13+
print("result is positive:", result)

src/libluau.zig

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1148,8 +1148,14 @@ pub const Lua = struct {
11481148

11491149
// luau_compile uses malloc to allocate the bytecode on the heap
11501150
defer zig_luau_free(bytecode);
1151+
try lua.loadBytecode("...", bytecode[0..size]);
1152+
}
11511153

1152-
if (c.luau_load(lua.state, "...", bytecode, size, 0) != 0) return error.Fail;
1154+
/// Loads bytecode binary (as compiled with f.ex. 'luau-compile --binary')
1155+
/// See https://luau-lang.org/getting-started
1156+
/// See also condsiderations for binary bytecode compatibility/safety: https://github.com/luau-lang/luau/issues/493#issuecomment-1185054665
1157+
pub fn loadBytecode(lua: *Lua, chunkname: [:0]const u8, bytecode: []const u8) !void {
1158+
if (c.luau_load(lua.state, chunkname.ptr, bytecode.ptr, bytecode.len, 0) != 0) return error.Fail;
11531159
}
11541160

11551161
/// If the registry already has the key `key`, returns an error
@@ -1486,3 +1492,36 @@ pub fn exportFn(comptime name: []const u8, comptime func: ZigFn) void {
14861492
const declaration = wrap(func);
14871493
@export(declaration, .{ .name = "luaopen_" ++ name, .linkage = .Strong });
14881494
}
1495+
1496+
/// Zig wrapper for Luau lua_CompileOptions that uses the same defaults as Luau if
1497+
/// no compile options is specified.
1498+
pub const CompileOptions = struct {
1499+
optimization_level: i32 = 1,
1500+
debug_level: i32 = 1,
1501+
coverage_level: i32 = 0,
1502+
/// global builtin to construct vectors; disabled by default (<vector_lib>.<vector_ctor>)
1503+
vector_lib: ?[*:0]const u8 = null,
1504+
vector_ctor: ?[*:0]const u8 = null,
1505+
/// vector type name for type tables; disabled by default
1506+
vector_type: ?[*:0]const u8 = null,
1507+
/// null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these
1508+
mutable_globals: ?[*:null]const ?[*:0]const u8 = null,
1509+
};
1510+
1511+
/// Compile luau source into bytecode, return callee owned buffer allocated through the given allocator.
1512+
pub fn compile(allocator: Allocator, source: []const u8, options: CompileOptions) ![]const u8 {
1513+
var size: usize = 0;
1514+
1515+
var opts = c.lua_CompileOptions{
1516+
.optimizationLevel = options.optimization_level,
1517+
.debugLevel = options.debug_level,
1518+
.coverageLevel = options.coverage_level,
1519+
.vectorLib = options.vector_lib,
1520+
.vectorCtor = options.vector_ctor,
1521+
.mutableGlobals = options.mutable_globals,
1522+
};
1523+
const bytecode = c.luau_compile(source.ptr, source.len, &opts, &size);
1524+
if (bytecode == null) return error.Memory;
1525+
defer zig_luau_free(bytecode);
1526+
return try allocator.dupe(u8, bytecode[0..size]);
1527+
}

src/tests.zig

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2152,3 +2152,35 @@ test "getstack" {
21522152
\\g()
21532153
);
21542154
}
2155+
2156+
test "compile and run bytecode" {
2157+
if (ziglua.lang != .luau) return;
2158+
2159+
var lua = try Lua.init(testing.allocator);
2160+
defer lua.deinit();
2161+
lua.openLibs();
2162+
2163+
// Load bytecode
2164+
const src = "return 133";
2165+
const bc = try ziglua.compile(testing.allocator, src, ziglua.CompileOptions{});
2166+
defer testing.allocator.free(bc);
2167+
2168+
try lua.loadBytecode("...", bc);
2169+
try lua.protectedCall(0, 1, 0);
2170+
const v = try lua.toInteger(-1);
2171+
try testing.expectEqual(@as(i32, 133), v);
2172+
2173+
// Try mutable globals. Calls to mutable globals should produce longer bytecode.
2174+
const src2 = "Foo.print()\nBar.print()";
2175+
const bc1 = try ziglua.compile(testing.allocator, src2, ziglua.CompileOptions{});
2176+
defer testing.allocator.free(bc1);
2177+
2178+
const options = ziglua.CompileOptions{
2179+
.mutable_globals = &[_:null]?[*:0]const u8{ "Foo", "Bar" },
2180+
};
2181+
const bc2 = try ziglua.compile(testing.allocator, src2, options);
2182+
defer testing.allocator.free(bc2);
2183+
// A really crude check for changed bytecode. Better would be to match
2184+
// produced bytecode in text format, but the API doesn't support it.
2185+
try testing.expect(bc1.len < bc2.len);
2186+
}

0 commit comments

Comments
 (0)