Skip to content

Expose Luau bytecode loading API, add example #38

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

Merged
merged 1 commit into from
Jan 12, 2024
Merged
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
6 changes: 5 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
@@ -69,10 +69,14 @@ pub fn build(b: *Build) void {
test_step.dependOn(&run_tests.step);

// Examples
const examples = [_]struct { []const u8, []const u8 }{
var common_examples = [_]struct { []const u8, []const u8 }{
.{ "interpreter", "examples/interpreter.zig" },
.{ "zig-function", "examples/zig-fn.zig" },
};
const luau_examples = [_]struct { []const u8, []const u8 }{
.{ "luau-bytecode", "examples/luau-bytecode.zig" },
};
const examples = if (lang == .luau) &common_examples ++ luau_examples else &common_examples;

for (examples) |example| {
const exe = b.addExecutable(.{
33 changes: 33 additions & 0 deletions examples/luau-bytecode.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Run Luau bytecode

// How to recompile `test.luau.bin` bytecode binary:
//
// luau-compile --binary test.luau > test.bc
//
// This may be required if the Luau version gets upgraded.

const std = @import("std");

// The ziglua module is made available in build.zig
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
var lua = try ziglua.Lua.init(allocator);
defer lua.deinit();

// Open all Lua standard libraries
lua.openLibs();

// Load bytecode
const src = @embedFile("./test.luau");
const bc = try ziglua.compile(allocator, src, ziglua.CompileOptions{});
defer allocator.free(bc);

try lua.loadBytecode("...", bc);
try lua.protectedCall(0, 0, 0);
}
13 changes: 13 additions & 0 deletions examples/test.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--!strict

function ispositive(x : number) : string
if x > 0 then
return "yes"
else
return "no"
end
end

local result : string
result = ispositive(1)
print("result is positive:", result)
41 changes: 40 additions & 1 deletion src/libluau.zig
Original file line number Diff line number Diff line change
@@ -1148,8 +1148,14 @@ pub const Lua = struct {

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

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

/// 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 {
const declaration = wrap(func);
@export(declaration, .{ .name = "luaopen_" ++ name, .linkage = .Strong });
}

/// Zig wrapper for Luau lua_CompileOptions that uses the same defaults as Luau if
/// no compile options is specified.
pub const CompileOptions = struct {
optimization_level: i32 = 1,
debug_level: i32 = 1,
coverage_level: i32 = 0,
/// global builtin to construct vectors; disabled by default (<vector_lib>.<vector_ctor>)
vector_lib: ?[*:0]const u8 = null,
vector_ctor: ?[*:0]const u8 = null,
/// vector type name for type tables; disabled by default
vector_type: ?[*:0]const u8 = null,
/// null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these
mutable_globals: ?[*:null]const ?[*:0]const u8 = null,
};

/// Compile luau source into bytecode, return callee owned buffer allocated through the given allocator.
pub fn compile(allocator: Allocator, source: []const u8, options: CompileOptions) ![]const u8 {
var size: usize = 0;

var opts = c.lua_CompileOptions{
.optimizationLevel = options.optimization_level,
.debugLevel = options.debug_level,
.coverageLevel = options.coverage_level,
.vectorLib = options.vector_lib,
.vectorCtor = options.vector_ctor,
.mutableGlobals = options.mutable_globals,
};
const bytecode = c.luau_compile(source.ptr, source.len, &opts, &size);
if (bytecode == null) return error.Memory;
defer zig_luau_free(bytecode);
return try allocator.dupe(u8, bytecode[0..size]);
}
32 changes: 32 additions & 0 deletions src/tests.zig
Original file line number Diff line number Diff line change
@@ -2152,3 +2152,35 @@ test "getstack" {
\\g()
);
}

test "compile and run bytecode" {
if (ziglua.lang != .luau) return;

var lua = try Lua.init(testing.allocator);
defer lua.deinit();
lua.openLibs();

// Load bytecode
const src = "return 133";
const bc = try ziglua.compile(testing.allocator, src, ziglua.CompileOptions{});
defer testing.allocator.free(bc);

try lua.loadBytecode("...", bc);
try lua.protectedCall(0, 1, 0);
const v = try lua.toInteger(-1);
try testing.expectEqual(@as(i32, 133), v);

// Try mutable globals. Calls to mutable globals should produce longer bytecode.
const src2 = "Foo.print()\nBar.print()";
const bc1 = try ziglua.compile(testing.allocator, src2, ziglua.CompileOptions{});
defer testing.allocator.free(bc1);

const options = ziglua.CompileOptions{
.mutable_globals = &[_:null]?[*:0]const u8{ "Foo", "Bar" },
};
const bc2 = try ziglua.compile(testing.allocator, src2, options);
defer testing.allocator.free(bc2);
// A really crude check for changed bytecode. Better would be to match
// produced bytecode in text format, but the API doesn't support it.
try testing.expect(bc1.len < bc2.len);
}