Modified how exports work and fixed memory leaks.

Now exports are already defined by the mods api
(something like preinit, init, preframe,
postframe, deinit should be enough functions). At
the moment we only support preinit function.
This commit is contained in:
Ernesto Lanchares 2025-04-04 18:34:58 +02:00
parent 28420f53b0
commit 25e51f9aea
3 changed files with 115 additions and 44 deletions

View file

@ -21,20 +21,21 @@ pub fn main() !void {
const file = try std.fs.cwd().openFile("assets/core.wasm", .{}); const file = try std.fs.cwd().openFile("assets/core.wasm", .{});
const all = try file.readToEndAlloc(allocator, 1_000_000); // 1 MB const all = try file.readToEndAlloc(allocator, 1_000_000); // 1 MB
var parser = mods.Parser{ defer allocator.free(all);
.bytes = all, var parser = try mods.Parser.init(allocator, all);
.byte_idx = 0, defer parser.deinit();
.allocator = allocator, parser.parseModule() catch |err| {
};
const module = parser.parseModule() catch |err| {
std.debug.print("[ERROR]: error at byte {x}(0x{x})\n", .{ parser.byte_idx, parser.bytes[parser.byte_idx] }); std.debug.print("[ERROR]: error at byte {x}(0x{x})\n", .{ parser.byte_idx, parser.bytes[parser.byte_idx] });
return err; return err;
}; };
const module = parser.module();
// defer module.deinit(allocator);
var runtime = try mods.Runtime.init(allocator, module, &global_runtime); var runtime = try mods.Runtime.init(allocator, module, &global_runtime);
defer runtime.deinit(allocator); defer runtime.deinit(allocator);
var parameters = [_]mods.VM.Value{.{ .i32 = 17 }}; var parameters = [_]mods.VM.Value{.{ .i32 = 17 }};
try runtime.callExternal(allocator, "preinit", &parameters); try runtime.callExternal(allocator, .preinit, &parameters);
const result = runtime.stack.pop().?; const result = runtime.stack.pop().?;
std.debug.print("Result of preinit: {any}\n", .{result}); std.debug.print("Result of preinit: {any}\n", .{result});
var w = try Renderer.Window.create(800, 600, "sideros"); var w = try Renderer.Window.create(800, 600, "sideros");

View file

@ -7,10 +7,10 @@ bytes: []const u8,
byte_idx: usize, byte_idx: usize,
allocator: Allocator, allocator: Allocator,
types: ?[]vm.Functype = null, types: []vm.Functype,
functions: ?[]vm.Function = null, functions: []vm.Function,
memory: ?Memtype = null, memory: Memtype,
exports: std.StringHashMapUnmanaged(u32) = .{}, exports: vm.Exports,
const Parser = @This(); const Parser = @This();
@ -33,10 +33,49 @@ pub const Error = error{
invalid_exportdesc, invalid_exportdesc,
double_else, double_else,
duplicated_funcsec, duplicated_funcsec,
duplicated_typesec,
unresolved_branch, unresolved_branch,
unterminated_wasm, unterminated_wasm,
}; };
pub fn init(allocator: Allocator, bytes: []const u8) !Parser {
return .{
.bytes = bytes,
.byte_idx = 0,
.allocator = allocator,
.types = &.{},
.functions = &.{},
.memory = .{
.lim = .{
.min = 0,
.max = 0,
},
},
.exports = .{},
};
}
pub fn deinit(self: Parser) void {
for (self.types) |t| {
self.allocator.free(t.parameters);
self.allocator.free(t.returns);
}
self.allocator.free(self.types);
self.allocator.free(self.functions);
}
pub fn module(self: *Parser) vm.Module {
defer self.functions = &.{};
return .{
.memory = .{
.min = self.memory.lim.min,
.max = self.memory.lim.max,
},
.functions = self.functions,
.exports = self.exports,
};
}
// TODO: This function should not exists // TODO: This function should not exists
fn warn(self: Parser, s: []const u8) void { fn warn(self: Parser, s: []const u8) void {
std.debug.print("[WARN]: Parsing of {s} unimplemented at byte index {d}\n", .{ s, self.byte_idx }); std.debug.print("[WARN]: Parsing of {s} unimplemented at byte index {d}\n", .{ s, self.byte_idx });
@ -231,7 +270,7 @@ fn parseGlobaltype(self: *Parser) !Globaltype {
// =========== // ===========
// NOTE: This should not return anything but modify IR // NOTE: This should not return anything but modify IR
pub fn parseModule(self: *Parser) !vm.Module { pub fn parseModule(self: *Parser) !void {
if (!std.mem.eql(u8, try self.read(4), &.{ 0x00, 0x61, 0x73, 0x6d })) return Error.invalid_magic; if (!std.mem.eql(u8, try self.read(4), &.{ 0x00, 0x61, 0x73, 0x6d })) return Error.invalid_magic;
if (!std.mem.eql(u8, try self.read(4), &.{ 0x01, 0x00, 0x00, 0x00 })) return Error.invalid_version; if (!std.mem.eql(u8, try self.read(4), &.{ 0x01, 0x00, 0x00, 0x00 })) return Error.invalid_version;
// TODO: Ensure only one section of each type (except for custom section), some code depends on it // TODO: Ensure only one section of each type (except for custom section), some code depends on it
@ -253,15 +292,6 @@ pub fn parseModule(self: *Parser) !vm.Module {
else => return Error.invalid_section, else => return Error.invalid_section,
}; };
} }
return .{
.memory = .{
.min = self.memory.?.lim.min,
.max = self.memory.?.lim.max,
},
.exports = self.exports,
.functions = self.functions.?,
};
} }
fn parseCustomsec(self: *Parser) !void { fn parseCustomsec(self: *Parser) !void {
@ -275,6 +305,8 @@ fn parseTypesec(self: *Parser) !void {
const end_idx = self.byte_idx + size; const end_idx = self.byte_idx + size;
const ft = try self.parseVector(Parser.parseFunctype); const ft = try self.parseVector(Parser.parseFunctype);
if (self.types.len != 0) return Error.duplicated_typesec;
self.types = ft; self.types = ft;
// TODO(ernesto): run this check not only on debug // TODO(ernesto): run this check not only on debug
@ -310,7 +342,7 @@ fn parseImportsec(self: *Parser) !void {
// TODO(ernesto): this should be used to do name resolution. // TODO(ernesto): this should be used to do name resolution.
const imports = try self.parseVector(Parser.parseImport); const imports = try self.parseVector(Parser.parseImport);
_ = imports; defer self.allocator.free(imports);
// TODO: run this check not only on debug // TODO: run this check not only on debug
std.debug.assert(self.byte_idx == end_idx); std.debug.assert(self.byte_idx == end_idx);
@ -321,16 +353,22 @@ fn parseFuncsec(self: *Parser) !void {
const end_idx = self.byte_idx + size; const end_idx = self.byte_idx + size;
const types = try self.parseVector(Parser.readU32); const types = try self.parseVector(Parser.readU32);
defer self.allocator.free(types);
if (self.functions != null) return Error.duplicated_funcsec; if (self.functions.len != 0) return Error.duplicated_funcsec;
self.functions = try self.allocator.alloc(vm.Function, types.len); self.functions = try self.allocator.alloc(vm.Function, types.len);
for (types, 0..) |t, i| { for (types, 0..) |t, i| {
self.functions.?[i].func_type = self.types.?[t]; self.functions[i].func_type = .{
.parameters = try self.allocator.alloc(vm.Valtype, self.types[t].parameters.len),
.returns = try self.allocator.alloc(vm.Valtype, self.types[t].returns.len),
};
@memcpy(self.functions[i].func_type.parameters, self.types[t].parameters);
@memcpy(self.functions[i].func_type.returns, self.types[t].returns);
} }
// TODO(ernesto): run this check not only in debug // TODO(ernesto): run this check not only in debug
std.debug.assert(types.len == self.functions.?.len); std.debug.assert(types.len == self.functions.len);
// TODO: run this check not only on debug // TODO: run this check not only on debug
std.debug.assert(self.byte_idx == end_idx); std.debug.assert(self.byte_idx == end_idx);
@ -347,6 +385,7 @@ fn parseMemsec(self: *Parser) !void {
const end_idx = self.byte_idx + size; const end_idx = self.byte_idx + size;
const mems = try self.parseVector(Parser.parseMemtype); const mems = try self.parseVector(Parser.parseMemtype);
defer self.allocator.free(mems);
if (mems.len == 0) { if (mems.len == 0) {
// WTF? // WTF?
} else if (mems.len == 1) { } else if (mems.len == 1) {
@ -391,9 +430,19 @@ fn parseExportsec(self: *Parser) !void {
const end_idx = self.byte_idx + size; const end_idx = self.byte_idx + size;
const exports = try self.parseVector(Parser.parseExport); const exports = try self.parseVector(Parser.parseExport);
defer {
for (exports) |e| self.allocator.free(e.name);
self.allocator.free(exports);
}
for (exports) |e| { for (exports) |e| {
switch (e.exportdesc) { switch (e.exportdesc) {
.func => try self.exports.put(self.allocator, e.name, e.exportdesc.func), .func => {
if (std.mem.eql(u8, e.name, "preinit")) {
self.exports.preinit = e.exportdesc.func;
} else {
std.log.warn("exported function {s} not supported\n", .{e.name});
}
},
else => std.debug.print("[WARN]: export ignored\n", .{}), else => std.debug.print("[WARN]: export ignored\n", .{}),
} }
} }
@ -434,6 +483,7 @@ fn parseCode(self: *Parser) !Func {
const end_idx = self.byte_idx + size; const end_idx = self.byte_idx + size;
const locals = try self.parseVector(Parser.parseLocal); const locals = try self.parseVector(Parser.parseLocal);
defer self.allocator.free(locals);
var local_count: usize = 0; var local_count: usize = 0;
for (locals) |l| { for (locals) |l| {
local_count += l.n; local_count += l.n;
@ -465,10 +515,11 @@ fn parseCodesec(self: *Parser) !void {
const end_idx = self.byte_idx + size; const end_idx = self.byte_idx + size;
const codes = try self.parseVector(Parser.parseCode); const codes = try self.parseVector(Parser.parseCode);
defer self.allocator.free(codes);
// TODO: run this check not only on debug // TODO: run this check not only on debug
std.debug.assert(codes.len == self.functions.?.len); std.debug.assert(codes.len == self.functions.len);
for (codes, self.functions.?) |code, *f| { for (codes, self.functions) |code, *f| {
f.typ = .{ .internal = .{ f.typ = .{ .internal = .{
.locals = code.locals, .locals = code.locals,
.ir = code.ir, .ir = code.ir,

View file

@ -24,22 +24,37 @@ pub const Functype = struct {
allocator.free(self.returns); allocator.free(self.returns);
} }
}; };
pub const Function = struct { func_type: Functype, typ: union(enum) { pub const Function = struct {
internal: struct { func_type: Functype,
locals: []Valtype, typ: union(enum) {
ir: IR, internal: struct {
}, locals: []Valtype,
external: void, ir: IR,
} }; },
external: void,
}
};
pub const ExportFunction = enum {
preinit,
};
pub const Exports = struct {
preinit: ?u32 = null,
};
comptime {
std.debug.assert(@typeInfo(ExportFunction).@"enum".fields.len == @typeInfo(Exports).@"struct".fields.len );
}
pub const Module = struct { pub const Module = struct {
memory: Memory, memory: Memory,
functions: []Function, functions: []Function,
exports: std.StringHashMapUnmanaged(u32), exports: Exports,
fn deinit(self: *Module, allocator: Allocator) void { pub fn deinit(self: Module, allocator: Allocator) void {
self.exports.deinit(allocator); // self.exports.deinit(allocator);
for (self.functions) |f| { for (self.functions) |f| {
std.debug.print("Freeing function parameters at {*}\n", .{f.func_type.parameters.ptr});
allocator.free(f.func_type.parameters); allocator.free(f.func_type.parameters);
allocator.free(f.func_type.returns); allocator.free(f.func_type.returns);
switch (f.typ) { switch (f.typ) {
@ -877,12 +892,16 @@ pub const Runtime = struct {
} }
// TODO: Do name resolution at parseTime // TODO: Do name resolution at parseTime
pub fn callExternal(self: *Runtime, allocator: Allocator, name: []const u8, parameters: []Value) !void { pub fn callExternal(self: *Runtime, allocator: Allocator, name: ExportFunction, parameters: []Value) !void {
if (self.module.exports.get(name)) |function| { switch (name) {
try self.call(allocator, function, parameters); .preinit => {
} else { if (self.module.exports.preinit) |func| {
std.debug.panic("Function `{s}` not avaliable", .{name}); try self.call(allocator, func, parameters);
} } else {
std.debug.panic("Function preinit unavailable\n", .{});
}
},
}
} }
pub fn call(self: *Runtime, allocator: Allocator, function: usize, parameters: []Value) AllocationError!void { pub fn call(self: *Runtime, allocator: Allocator, function: usize, parameters: []Value) AllocationError!void {