* wasm: Move wasm's codegen to arch/wasm/CodeGen.zig * wasm: Define Wasm's Mir This declares the initial most-used instructions for wasm as well as the data that represents them. TODO: Add binary operand opcodes. By re-using the wasm opcode values, we can emit each opcode very easily by simply using `@enumToInt()`. However, this poses a possible problem: If we use all of wasm's opcodes, it leaves us no room to use synthetic opcodes such as debugging instructions. We could use reserved opcodes, but the wasm spec may use them at some point. TODO: Check if we should perhaps use a 16bit tag where the highest bits are used for synthetic opcodes. * wasm: Define basic Emit structure * wasm: Implement corresponding Emit functions for MIR * wasm: Initial lowering to MIR - This implements lowering to MIR from AIR for storing and loading of locals as well as emitting immediates. - Relocating function indexes has been simplified a lot as well as we no longer need to patch offsets and we write a relocatable value instead. - Locals are now emitted at the beginning of the function section entry meaning all offsets we generate are stable. * wasm: Lower all AIR instructions to MIR * wasm: Implement remaining MIR instructions * wasm: Fix function relocations * wasm: Get all tests working * wasm: Make `Data` 4 bytes instead of 8. - 64bit immediates are now stored in 2 seperate u32's. - 64bit floats are now stored in 2 seperate u32's. - `mem_arg` is now stored as a seperate payload in extra.
986 lines
37 KiB
Zig
986 lines
37 KiB
Zig
const Wasm = @This();
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const mem = std.mem;
|
|
const Allocator = std.mem.Allocator;
|
|
const assert = std.debug.assert;
|
|
const fs = std.fs;
|
|
const leb = std.leb;
|
|
const log = std.log.scoped(.link);
|
|
const wasm = std.wasm;
|
|
|
|
const Module = @import("../Module.zig");
|
|
const Compilation = @import("../Compilation.zig");
|
|
const CodeGen = @import("../arch/wasm/CodeGen.zig");
|
|
const link = @import("../link.zig");
|
|
const trace = @import("../tracy.zig").trace;
|
|
const build_options = @import("build_options");
|
|
const wasi_libc = @import("../wasi_libc.zig");
|
|
const Cache = @import("../Cache.zig");
|
|
const TypedValue = @import("../TypedValue.zig");
|
|
const LlvmObject = @import("../codegen/llvm.zig").Object;
|
|
const Air = @import("../Air.zig");
|
|
const Liveness = @import("../Liveness.zig");
|
|
|
|
pub const base_tag = link.File.Tag.wasm;
|
|
|
|
base: link.File,
|
|
/// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
|
|
llvm_object: ?*LlvmObject = null,
|
|
/// List of all function Decls to be written to the output file. The index of
|
|
/// each Decl in this list at the time of writing the binary is used as the
|
|
/// function index. In the event where ext_funcs' size is not 0, the index of
|
|
/// each function is added on top of the ext_funcs' length.
|
|
/// TODO: can/should we access some data structure in Module directly?
|
|
funcs: std.ArrayListUnmanaged(*Module.Decl) = .{},
|
|
/// List of all extern function Decls to be written to the `import` section of the
|
|
/// wasm binary. The position in the list defines the function index
|
|
ext_funcs: std.ArrayListUnmanaged(*Module.Decl) = .{},
|
|
/// When importing objects from the host environment, a name must be supplied.
|
|
/// LLVM uses "env" by default when none is given. This would be a good default for Zig
|
|
/// to support existing code.
|
|
/// TODO: Allow setting this through a flag?
|
|
host_name: []const u8 = "env",
|
|
/// The last `DeclBlock` that was initialized will be saved here.
|
|
last_block: ?*DeclBlock = null,
|
|
/// Table with offsets, each element represents an offset with the value being
|
|
/// the offset into the 'data' section where the data lives
|
|
offset_table: std.ArrayListUnmanaged(u32) = .{},
|
|
/// List of offset indexes which are free to be used for new decl's.
|
|
/// Each element's value points to an index into the offset_table.
|
|
offset_table_free_list: std.ArrayListUnmanaged(u32) = .{},
|
|
/// List of all `Decl` that are currently alive.
|
|
/// This is ment for bookkeeping so we can safely cleanup all codegen memory
|
|
/// when calling `deinit`
|
|
symbols: std.ArrayListUnmanaged(*Module.Decl) = .{},
|
|
/// List of symbol indexes which are free to be used.
|
|
symbols_free_list: std.ArrayListUnmanaged(u32) = .{},
|
|
|
|
pub const FnData = struct {
|
|
/// Generated code for the type of the function
|
|
functype: std.ArrayListUnmanaged(u8),
|
|
/// Generated code for the body of the function
|
|
code: std.ArrayListUnmanaged(u8),
|
|
/// Locations in the generated code where function indexes must be filled in.
|
|
/// This must be kept ordered by offset.
|
|
/// `decl` is the symbol_index of the target.
|
|
idx_refs: std.ArrayListUnmanaged(struct { offset: u32, decl: u32 }),
|
|
|
|
pub const empty: FnData = .{
|
|
.functype = .{},
|
|
.code = .{},
|
|
.idx_refs = .{},
|
|
};
|
|
};
|
|
|
|
pub const DeclBlock = struct {
|
|
/// Determines whether the `DeclBlock` has been initialized for codegen.
|
|
init: bool,
|
|
/// Index into the `symbols` list.
|
|
symbol_index: u32,
|
|
/// Index into the offset table
|
|
offset_index: u32,
|
|
/// The size of the block and how large part of the data section it occupies.
|
|
/// Will be 0 when the Decl will not live inside the data section and `data` will be undefined.
|
|
size: u32,
|
|
/// Points to the previous and next blocks.
|
|
/// Can be used to find the total size, and used to calculate the `offset` based on the previous block.
|
|
prev: ?*DeclBlock,
|
|
next: ?*DeclBlock,
|
|
/// Pointer to data that will be written to the 'data' section.
|
|
/// This data either lives in `FnData.code` or is externally managed.
|
|
/// For data that does not live inside the 'data' section, this field will be undefined. (size == 0).
|
|
data: [*]const u8,
|
|
|
|
pub const empty: DeclBlock = .{
|
|
.init = false,
|
|
.symbol_index = 0,
|
|
.offset_index = 0,
|
|
.size = 0,
|
|
.prev = null,
|
|
.next = null,
|
|
.data = undefined,
|
|
};
|
|
|
|
/// Unplugs the `DeclBlock` from the chain
|
|
fn unplug(self: *DeclBlock) void {
|
|
if (self.prev) |prev| {
|
|
prev.next = self.next;
|
|
}
|
|
|
|
if (self.next) |next| {
|
|
next.prev = self.prev;
|
|
}
|
|
self.next = null;
|
|
self.prev = null;
|
|
}
|
|
};
|
|
|
|
pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Options) !*Wasm {
|
|
assert(options.object_format == .wasm);
|
|
|
|
if (build_options.have_llvm and options.use_llvm) {
|
|
const self = try createEmpty(allocator, options);
|
|
errdefer self.base.destroy();
|
|
|
|
self.llvm_object = try LlvmObject.create(allocator, sub_path, options);
|
|
return self;
|
|
}
|
|
|
|
// TODO: read the file and keep valid parts instead of truncating
|
|
const file = try options.emit.?.directory.handle.createFile(sub_path, .{ .truncate = true, .read = true });
|
|
errdefer file.close();
|
|
|
|
const wasm_bin = try createEmpty(allocator, options);
|
|
errdefer wasm_bin.base.destroy();
|
|
|
|
wasm_bin.base.file = file;
|
|
|
|
try file.writeAll(&(wasm.magic ++ wasm.version));
|
|
|
|
return wasm_bin;
|
|
}
|
|
|
|
pub fn createEmpty(gpa: *Allocator, options: link.Options) !*Wasm {
|
|
const wasm_bin = try gpa.create(Wasm);
|
|
wasm_bin.* = .{
|
|
.base = .{
|
|
.tag = .wasm,
|
|
.options = options,
|
|
.file = null,
|
|
.allocator = gpa,
|
|
},
|
|
};
|
|
return wasm_bin;
|
|
}
|
|
|
|
pub fn deinit(self: *Wasm) void {
|
|
if (build_options.have_llvm) {
|
|
if (self.llvm_object) |llvm_object| llvm_object.destroy(self.base.allocator);
|
|
}
|
|
|
|
for (self.symbols.items) |decl, symbol_index| {
|
|
// Check if we already freed all memory for the symbol
|
|
// TODO: Audit this when we refactor the linker.
|
|
var already_freed = false;
|
|
for (self.symbols_free_list.items) |index| {
|
|
if (symbol_index == index) {
|
|
already_freed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (already_freed) continue;
|
|
decl.fn_link.wasm.functype.deinit(self.base.allocator);
|
|
decl.fn_link.wasm.code.deinit(self.base.allocator);
|
|
decl.fn_link.wasm.idx_refs.deinit(self.base.allocator);
|
|
}
|
|
|
|
self.funcs.deinit(self.base.allocator);
|
|
self.ext_funcs.deinit(self.base.allocator);
|
|
self.offset_table.deinit(self.base.allocator);
|
|
self.offset_table_free_list.deinit(self.base.allocator);
|
|
self.symbols.deinit(self.base.allocator);
|
|
self.symbols_free_list.deinit(self.base.allocator);
|
|
}
|
|
|
|
pub fn allocateDeclIndexes(self: *Wasm, decl: *Module.Decl) !void {
|
|
if (decl.link.wasm.init) return;
|
|
|
|
try self.offset_table.ensureUnusedCapacity(self.base.allocator, 1);
|
|
try self.symbols.ensureUnusedCapacity(self.base.allocator, 1);
|
|
|
|
const block = &decl.link.wasm;
|
|
block.init = true;
|
|
|
|
if (self.offset_table_free_list.popOrNull()) |index| {
|
|
block.offset_index = index;
|
|
} else {
|
|
block.offset_index = @intCast(u32, self.offset_table.items.len);
|
|
_ = self.offset_table.addOneAssumeCapacity();
|
|
}
|
|
|
|
if (self.symbols_free_list.popOrNull()) |index| {
|
|
block.symbol_index = index;
|
|
self.symbols.items[block.symbol_index] = decl;
|
|
} else {
|
|
block.symbol_index = @intCast(u32, self.symbols.items.len);
|
|
self.symbols.appendAssumeCapacity(decl);
|
|
}
|
|
|
|
self.offset_table.items[block.offset_index] = 0;
|
|
|
|
if (decl.ty.zigTypeTag() == .Fn) {
|
|
switch (decl.val.tag()) {
|
|
// dependent on function type, appends it to the correct list
|
|
.function => try self.funcs.append(self.base.allocator, decl),
|
|
.extern_fn => try self.ext_funcs.append(self.base.allocator, decl),
|
|
else => unreachable,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn updateFunc(self: *Wasm, module: *Module, func: *Module.Fn, air: Air, liveness: Liveness) !void {
|
|
if (build_options.skip_non_native and builtin.object_format != .wasm) {
|
|
@panic("Attempted to compile for object format that was disabled by build configuration");
|
|
}
|
|
if (build_options.have_llvm) {
|
|
if (self.llvm_object) |llvm_object| return llvm_object.updateFunc(module, func, air, liveness);
|
|
}
|
|
const decl = func.owner_decl;
|
|
assert(decl.link.wasm.init); // Must call allocateDeclIndexes()
|
|
|
|
const fn_data = &decl.fn_link.wasm;
|
|
fn_data.functype.items.len = 0;
|
|
fn_data.code.items.len = 0;
|
|
fn_data.idx_refs.items.len = 0;
|
|
|
|
var codegen: CodeGen = .{
|
|
.gpa = self.base.allocator,
|
|
.air = air,
|
|
.liveness = liveness,
|
|
.values = .{},
|
|
.code = fn_data.code.toManaged(self.base.allocator),
|
|
.func_type_data = fn_data.functype.toManaged(self.base.allocator),
|
|
.decl = decl,
|
|
.err_msg = undefined,
|
|
.locals = .{},
|
|
.target = self.base.options.target,
|
|
.bin_file = &self.base,
|
|
.global_error_set = self.base.options.module.?.global_error_set,
|
|
};
|
|
defer codegen.deinit();
|
|
|
|
// generate the 'code' section for the function declaration
|
|
const result = codegen.genFunc() catch |err| switch (err) {
|
|
error.CodegenFail => {
|
|
decl.analysis = .codegen_failure;
|
|
try module.failed_decls.put(module.gpa, decl, codegen.err_msg);
|
|
return;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
return self.finishUpdateDecl(decl, result, &codegen);
|
|
}
|
|
|
|
// Generate code for the Decl, storing it in memory to be later written to
|
|
// the file on flush().
|
|
pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void {
|
|
if (build_options.skip_non_native and builtin.object_format != .wasm) {
|
|
@panic("Attempted to compile for object format that was disabled by build configuration");
|
|
}
|
|
if (build_options.have_llvm) {
|
|
if (self.llvm_object) |llvm_object| return llvm_object.updateDecl(module, decl);
|
|
}
|
|
assert(decl.link.wasm.init); // Must call allocateDeclIndexes()
|
|
|
|
// TODO don't use this for non-functions
|
|
const fn_data = &decl.fn_link.wasm;
|
|
fn_data.functype.items.len = 0;
|
|
fn_data.code.items.len = 0;
|
|
fn_data.idx_refs.items.len = 0;
|
|
|
|
var codegen: CodeGen = .{
|
|
.gpa = self.base.allocator,
|
|
.air = undefined,
|
|
.liveness = undefined,
|
|
.values = .{},
|
|
.code = fn_data.code.toManaged(self.base.allocator),
|
|
.func_type_data = fn_data.functype.toManaged(self.base.allocator),
|
|
.decl = decl,
|
|
.err_msg = undefined,
|
|
.locals = .{},
|
|
.target = self.base.options.target,
|
|
.bin_file = &self.base,
|
|
.global_error_set = self.base.options.module.?.global_error_set,
|
|
};
|
|
defer codegen.deinit();
|
|
|
|
// generate the 'code' section for the function declaration
|
|
const result = codegen.gen(decl.ty, decl.val) catch |err| switch (err) {
|
|
error.CodegenFail => {
|
|
decl.analysis = .codegen_failure;
|
|
try module.failed_decls.put(module.gpa, decl, codegen.err_msg);
|
|
return;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
|
|
return self.finishUpdateDecl(decl, result, &codegen);
|
|
}
|
|
|
|
fn finishUpdateDecl(self: *Wasm, decl: *Module.Decl, result: CodeGen.Result, codegen: *CodeGen) !void {
|
|
const fn_data: *FnData = &decl.fn_link.wasm;
|
|
|
|
fn_data.code = codegen.code.toUnmanaged();
|
|
fn_data.functype = codegen.func_type_data.toUnmanaged();
|
|
|
|
const code: []const u8 = switch (result) {
|
|
.appended => @as([]const u8, fn_data.code.items),
|
|
.externally_managed => |payload| payload,
|
|
};
|
|
|
|
const block = &decl.link.wasm;
|
|
if (decl.ty.zigTypeTag() != .Fn) {
|
|
block.size = @intCast(u32, code.len);
|
|
block.data = code.ptr;
|
|
}
|
|
|
|
// If we're updating an existing decl, unplug it first
|
|
// to avoid infinite loops due to earlier links
|
|
block.unplug();
|
|
|
|
if (self.last_block) |last| {
|
|
if (last != block) {
|
|
last.next = block;
|
|
block.prev = last;
|
|
}
|
|
}
|
|
self.last_block = block;
|
|
}
|
|
|
|
pub fn updateDeclExports(
|
|
self: *Wasm,
|
|
module: *Module,
|
|
decl: *const Module.Decl,
|
|
exports: []const *Module.Export,
|
|
) !void {
|
|
if (build_options.skip_non_native and builtin.object_format != .wasm) {
|
|
@panic("Attempted to compile for object format that was disabled by build configuration");
|
|
}
|
|
if (build_options.have_llvm) {
|
|
if (self.llvm_object) |llvm_object| return llvm_object.updateDeclExports(module, decl, exports);
|
|
}
|
|
}
|
|
|
|
pub fn freeDecl(self: *Wasm, decl: *Module.Decl) void {
|
|
if (build_options.have_llvm) {
|
|
if (self.llvm_object) |llvm_object| return llvm_object.freeDecl(decl);
|
|
}
|
|
|
|
if (self.getFuncidx(decl)) |func_idx| {
|
|
switch (decl.val.tag()) {
|
|
.function => _ = self.funcs.swapRemove(func_idx),
|
|
.extern_fn => _ = self.ext_funcs.swapRemove(func_idx),
|
|
else => unreachable,
|
|
}
|
|
}
|
|
const block = &decl.link.wasm;
|
|
|
|
if (self.last_block == block) {
|
|
self.last_block = block.prev;
|
|
}
|
|
|
|
block.unplug();
|
|
|
|
self.offset_table_free_list.append(self.base.allocator, decl.link.wasm.offset_index) catch {};
|
|
self.symbols_free_list.append(self.base.allocator, block.symbol_index) catch {};
|
|
|
|
block.init = false;
|
|
|
|
decl.fn_link.wasm.functype.deinit(self.base.allocator);
|
|
decl.fn_link.wasm.code.deinit(self.base.allocator);
|
|
decl.fn_link.wasm.idx_refs.deinit(self.base.allocator);
|
|
}
|
|
|
|
pub fn flush(self: *Wasm, comp: *Compilation) !void {
|
|
if (build_options.have_llvm and self.base.options.use_lld) {
|
|
return self.linkWithLLD(comp);
|
|
} else {
|
|
return self.flushModule(comp);
|
|
}
|
|
}
|
|
|
|
pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
|
|
_ = comp;
|
|
const tracy = trace(@src());
|
|
defer tracy.end();
|
|
|
|
const file = self.base.file.?;
|
|
const header_size = 5 + 1;
|
|
// ptr_width in bytes
|
|
const ptr_width = self.base.options.target.cpu.arch.ptrBitWidth() / 8;
|
|
// The size of the offset table in bytes
|
|
// The table contains all decl's with its corresponding offset into
|
|
// the 'data' section
|
|
const offset_table_size = @intCast(u32, self.offset_table.items.len * ptr_width);
|
|
|
|
// The size of the data, this together with `offset_table_size` amounts to the
|
|
// total size of the 'data' section
|
|
var first_decl: ?*DeclBlock = null;
|
|
const data_size: u32 = if (self.last_block) |last| blk: {
|
|
var size = last.size;
|
|
var cur = last;
|
|
while (cur.prev) |prev| : (cur = prev) {
|
|
size += prev.size;
|
|
}
|
|
first_decl = cur;
|
|
break :blk size;
|
|
} else 0;
|
|
|
|
// No need to rewrite the magic/version header
|
|
try file.setEndPos(@sizeOf(@TypeOf(wasm.magic ++ wasm.version)));
|
|
try file.seekTo(@sizeOf(@TypeOf(wasm.magic ++ wasm.version)));
|
|
|
|
// Type section
|
|
{
|
|
const header_offset = try reserveVecSectionHeader(file);
|
|
|
|
// extern functions are defined in the wasm binary first through the `import`
|
|
// section, so define their func types first
|
|
for (self.ext_funcs.items) |decl| try file.writeAll(decl.fn_link.wasm.functype.items);
|
|
for (self.funcs.items) |decl| try file.writeAll(decl.fn_link.wasm.functype.items);
|
|
|
|
try writeVecSectionHeader(
|
|
file,
|
|
header_offset,
|
|
.type,
|
|
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
|
@intCast(u32, self.ext_funcs.items.len + self.funcs.items.len),
|
|
);
|
|
}
|
|
|
|
// Import section
|
|
{
|
|
// TODO: implement non-functions imports
|
|
const header_offset = try reserveVecSectionHeader(file);
|
|
const writer = file.writer();
|
|
for (self.ext_funcs.items) |decl, typeidx| {
|
|
try leb.writeULEB128(writer, @intCast(u32, self.host_name.len));
|
|
try writer.writeAll(self.host_name);
|
|
|
|
// wasm requires the length of the import name with no null-termination
|
|
const decl_len = mem.len(decl.name);
|
|
try leb.writeULEB128(writer, @intCast(u32, decl_len));
|
|
try writer.writeAll(decl.name[0..decl_len]);
|
|
|
|
// emit kind and the function type
|
|
try writer.writeByte(wasm.externalKind(.function));
|
|
try leb.writeULEB128(writer, @intCast(u32, typeidx));
|
|
}
|
|
|
|
try writeVecSectionHeader(
|
|
file,
|
|
header_offset,
|
|
.import,
|
|
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
|
@intCast(u32, self.ext_funcs.items.len),
|
|
);
|
|
}
|
|
|
|
// Function section
|
|
{
|
|
const header_offset = try reserveVecSectionHeader(file);
|
|
const writer = file.writer();
|
|
for (self.funcs.items) |_, typeidx| {
|
|
const func_idx = @intCast(u32, self.getFuncIdxOffset() + typeidx);
|
|
try leb.writeULEB128(writer, func_idx);
|
|
}
|
|
|
|
try writeVecSectionHeader(
|
|
file,
|
|
header_offset,
|
|
.function,
|
|
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
|
@intCast(u32, self.funcs.items.len),
|
|
);
|
|
}
|
|
|
|
// Memory section
|
|
if (data_size != 0) {
|
|
const header_offset = try reserveVecSectionHeader(file);
|
|
const writer = file.writer();
|
|
|
|
try leb.writeULEB128(writer, @as(u32, 0));
|
|
// Calculate the amount of memory pages are required and write them.
|
|
// Wasm uses 64kB page sizes. Round up to ensure the data segments fit into the memory
|
|
try leb.writeULEB128(
|
|
writer,
|
|
try std.math.divCeil(
|
|
u32,
|
|
offset_table_size + data_size,
|
|
std.wasm.page_size,
|
|
),
|
|
);
|
|
try writeVecSectionHeader(
|
|
file,
|
|
header_offset,
|
|
.memory,
|
|
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
|
@as(u32, 1), // wasm currently only supports 1 linear memory segment
|
|
);
|
|
}
|
|
|
|
// Export section
|
|
if (self.base.options.module) |module| {
|
|
const header_offset = try reserveVecSectionHeader(file);
|
|
const writer = file.writer();
|
|
var count: u32 = 0;
|
|
for (module.decl_exports.values()) |exports| {
|
|
for (exports) |exprt| {
|
|
// Export name length + name
|
|
try leb.writeULEB128(writer, @intCast(u32, exprt.options.name.len));
|
|
try writer.writeAll(exprt.options.name);
|
|
|
|
switch (exprt.exported_decl.ty.zigTypeTag()) {
|
|
.Fn => {
|
|
// Type of the export
|
|
try writer.writeByte(wasm.externalKind(.function));
|
|
// Exported function index
|
|
try leb.writeULEB128(writer, self.getFuncidx(exprt.exported_decl).?);
|
|
},
|
|
else => return error.TODOImplementNonFnDeclsForWasm,
|
|
}
|
|
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
// export memory if size is not 0
|
|
if (data_size != 0) {
|
|
try leb.writeULEB128(writer, @intCast(u32, "memory".len));
|
|
try writer.writeAll("memory");
|
|
try writer.writeByte(wasm.externalKind(.memory));
|
|
try leb.writeULEB128(writer, @as(u32, 0)); // only 1 memory 'object' can exist
|
|
count += 1;
|
|
}
|
|
|
|
try writeVecSectionHeader(
|
|
file,
|
|
header_offset,
|
|
.@"export",
|
|
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
|
count,
|
|
);
|
|
}
|
|
|
|
// Code section
|
|
{
|
|
const header_offset = try reserveVecSectionHeader(file);
|
|
const writer = file.writer();
|
|
for (self.funcs.items) |decl| {
|
|
const fn_data = &decl.fn_link.wasm;
|
|
|
|
// Write the already generated code to the file, inserting
|
|
// function indexes where required.
|
|
for (fn_data.idx_refs.items) |idx_ref| {
|
|
const relocatable_decl = self.symbols.items[idx_ref.decl];
|
|
const index = self.getFuncidx(relocatable_decl).?;
|
|
leb.writeUnsignedFixed(5, fn_data.code.items[idx_ref.offset..][0..5], index);
|
|
}
|
|
try writer.writeAll(fn_data.code.items);
|
|
}
|
|
try writeVecSectionHeader(
|
|
file,
|
|
header_offset,
|
|
.code,
|
|
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
|
@intCast(u32, self.funcs.items.len),
|
|
);
|
|
}
|
|
|
|
// Data section
|
|
if (data_size != 0) {
|
|
const header_offset = try reserveVecSectionHeader(file);
|
|
const writer = file.writer();
|
|
// index to memory section (currently, there can only be 1 memory section in wasm)
|
|
try leb.writeULEB128(writer, @as(u32, 0));
|
|
|
|
// offset into data section
|
|
try writer.writeByte(wasm.opcode(.i32_const));
|
|
try leb.writeILEB128(writer, @as(i32, 0));
|
|
try writer.writeByte(wasm.opcode(.end));
|
|
|
|
const total_size = offset_table_size + data_size;
|
|
|
|
// offset table + data size
|
|
try leb.writeULEB128(writer, total_size);
|
|
|
|
// fill in the offset table and the data segments
|
|
const file_offset = try file.getPos();
|
|
var cur = first_decl;
|
|
var data_offset = offset_table_size;
|
|
while (cur) |cur_block| : (cur = cur_block.next) {
|
|
if (cur_block.size == 0) continue;
|
|
assert(cur_block.init);
|
|
|
|
const offset = (cur_block.offset_index) * ptr_width;
|
|
var buf: [4]u8 = undefined;
|
|
std.mem.writeIntLittle(u32, &buf, data_offset);
|
|
|
|
try file.pwriteAll(&buf, file_offset + offset);
|
|
try file.pwriteAll(cur_block.data[0..cur_block.size], file_offset + data_offset);
|
|
data_offset += cur_block.size;
|
|
}
|
|
|
|
try file.seekTo(file_offset + data_offset);
|
|
try writeVecSectionHeader(
|
|
file,
|
|
header_offset,
|
|
.data,
|
|
@intCast(u32, (file_offset + data_offset) - header_offset - header_size),
|
|
@intCast(u32, 1), // only 1 data section
|
|
);
|
|
}
|
|
}
|
|
|
|
fn linkWithLLD(self: *Wasm, comp: *Compilation) !void {
|
|
const tracy = trace(@src());
|
|
defer tracy.end();
|
|
|
|
var arena_allocator = std.heap.ArenaAllocator.init(self.base.allocator);
|
|
defer arena_allocator.deinit();
|
|
const arena = &arena_allocator.allocator;
|
|
|
|
const directory = self.base.options.emit.?.directory; // Just an alias to make it shorter to type.
|
|
|
|
// If there is no Zig code to compile, then we should skip flushing the output file because it
|
|
// will not be part of the linker line anyway.
|
|
const module_obj_path: ?[]const u8 = if (self.base.options.module) |module| blk: {
|
|
const use_stage1 = build_options.is_stage1 and self.base.options.use_stage1;
|
|
if (use_stage1) {
|
|
const obj_basename = try std.zig.binNameAlloc(arena, .{
|
|
.root_name = self.base.options.root_name,
|
|
.target = self.base.options.target,
|
|
.output_mode = .Obj,
|
|
});
|
|
const o_directory = module.zig_cache_artifact_directory;
|
|
const full_obj_path = try o_directory.join(arena, &[_][]const u8{obj_basename});
|
|
break :blk full_obj_path;
|
|
}
|
|
|
|
try self.flushModule(comp);
|
|
const obj_basename = self.base.intermediary_basename.?;
|
|
const full_obj_path = try directory.join(arena, &[_][]const u8{obj_basename});
|
|
break :blk full_obj_path;
|
|
} else null;
|
|
|
|
const is_obj = self.base.options.output_mode == .Obj;
|
|
|
|
const compiler_rt_path: ?[]const u8 = if (self.base.options.include_compiler_rt and !is_obj)
|
|
comp.compiler_rt_static_lib.?.full_object_path
|
|
else
|
|
null;
|
|
|
|
const target = self.base.options.target;
|
|
|
|
const id_symlink_basename = "lld.id";
|
|
|
|
var man: Cache.Manifest = undefined;
|
|
defer if (!self.base.options.disable_lld_caching) man.deinit();
|
|
|
|
var digest: [Cache.hex_digest_len]u8 = undefined;
|
|
|
|
if (!self.base.options.disable_lld_caching) {
|
|
man = comp.cache_parent.obtain();
|
|
|
|
// We are about to obtain this lock, so here we give other processes a chance first.
|
|
self.base.releaseLock();
|
|
|
|
try man.addListOfFiles(self.base.options.objects);
|
|
for (comp.c_object_table.keys()) |key| {
|
|
_ = try man.addFile(key.status.success.object_path, null);
|
|
}
|
|
try man.addOptionalFile(module_obj_path);
|
|
try man.addOptionalFile(compiler_rt_path);
|
|
man.hash.addOptional(self.base.options.stack_size_override);
|
|
man.hash.addListOfBytes(self.base.options.extra_lld_args);
|
|
man.hash.add(self.base.options.import_memory);
|
|
man.hash.addOptional(self.base.options.initial_memory);
|
|
man.hash.addOptional(self.base.options.max_memory);
|
|
man.hash.addOptional(self.base.options.global_base);
|
|
|
|
// We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
|
|
_ = try man.hit();
|
|
digest = man.final();
|
|
|
|
var prev_digest_buf: [digest.len]u8 = undefined;
|
|
const prev_digest: []u8 = Cache.readSmallFile(
|
|
directory.handle,
|
|
id_symlink_basename,
|
|
&prev_digest_buf,
|
|
) catch |err| blk: {
|
|
log.debug("WASM LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
|
|
// Handle this as a cache miss.
|
|
break :blk prev_digest_buf[0..0];
|
|
};
|
|
if (mem.eql(u8, prev_digest, &digest)) {
|
|
log.debug("WASM LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
|
|
// Hot diggity dog! The output binary is already there.
|
|
self.base.lock = man.toOwnedLock();
|
|
return;
|
|
}
|
|
log.debug("WASM LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });
|
|
|
|
// We are about to change the output file to be different, so we invalidate the build hash now.
|
|
directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
|
|
error.FileNotFound => {},
|
|
else => |e| return e,
|
|
};
|
|
}
|
|
|
|
const full_out_path = try directory.join(arena, &[_][]const u8{self.base.options.emit.?.sub_path});
|
|
|
|
if (self.base.options.output_mode == .Obj) {
|
|
// LLD's WASM driver does not support the equivalent of `-r` so we do a simple file copy
|
|
// here. TODO: think carefully about how we can avoid this redundant operation when doing
|
|
// build-obj. See also the corresponding TODO in linkAsArchive.
|
|
const the_object_path = blk: {
|
|
if (self.base.options.objects.len != 0)
|
|
break :blk self.base.options.objects[0];
|
|
|
|
if (comp.c_object_table.count() != 0)
|
|
break :blk comp.c_object_table.keys()[0].status.success.object_path;
|
|
|
|
if (module_obj_path) |p|
|
|
break :blk p;
|
|
|
|
// TODO I think this is unreachable. Audit this situation when solving the above TODO
|
|
// regarding eliding redundant object -> object transformations.
|
|
return error.NoObjectsToLink;
|
|
};
|
|
// This can happen when using --enable-cache and using the stage1 backend. In this case
|
|
// we can skip the file copy.
|
|
if (!mem.eql(u8, the_object_path, full_out_path)) {
|
|
try fs.cwd().copyFile(the_object_path, fs.cwd(), full_out_path, .{});
|
|
}
|
|
} else {
|
|
// Create an LLD command line and invoke it.
|
|
var argv = std.ArrayList([]const u8).init(self.base.allocator);
|
|
defer argv.deinit();
|
|
// We will invoke ourselves as a child process to gain access to LLD.
|
|
// This is necessary because LLD does not behave properly as a library -
|
|
// it calls exit() and does not reset all global data between invocations.
|
|
try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "wasm-ld" });
|
|
try argv.append("-error-limit=0");
|
|
|
|
if (self.base.options.lto) {
|
|
switch (self.base.options.optimize_mode) {
|
|
.Debug => {},
|
|
.ReleaseSmall => try argv.append("-O2"),
|
|
.ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
|
|
}
|
|
}
|
|
|
|
if (self.base.options.import_memory) {
|
|
try argv.append("--import-memory");
|
|
}
|
|
|
|
if (self.base.options.initial_memory) |initial_memory| {
|
|
const arg = try std.fmt.allocPrint(arena, "--initial-memory={d}", .{initial_memory});
|
|
try argv.append(arg);
|
|
}
|
|
|
|
if (self.base.options.max_memory) |max_memory| {
|
|
const arg = try std.fmt.allocPrint(arena, "--max-memory={d}", .{max_memory});
|
|
try argv.append(arg);
|
|
}
|
|
|
|
if (self.base.options.global_base) |global_base| {
|
|
const arg = try std.fmt.allocPrint(arena, "--global-base={d}", .{global_base});
|
|
try argv.append(arg);
|
|
}
|
|
|
|
if (self.base.options.output_mode == .Exe) {
|
|
// Increase the default stack size to a more reasonable value of 1MB instead of
|
|
// the default of 1 Wasm page being 64KB, unless overridden by the user.
|
|
try argv.append("-z");
|
|
const stack_size = self.base.options.stack_size_override orelse 1048576;
|
|
const arg = try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size});
|
|
try argv.append(arg);
|
|
|
|
// Put stack before globals so that stack overflow results in segfault immediately
|
|
// before corrupting globals. See https://github.com/ziglang/zig/issues/4496
|
|
try argv.append("--stack-first");
|
|
|
|
if (self.base.options.wasi_exec_model == .reactor) {
|
|
// Reactor execution model does not have _start so lld doesn't look for it.
|
|
try argv.append("--no-entry");
|
|
// Make sure "_initialize" and other used-defined functions are exported if this is WASI reactor.
|
|
try argv.append("--export-dynamic");
|
|
}
|
|
} else {
|
|
if (self.base.options.stack_size_override) |stack_size| {
|
|
try argv.append("-z");
|
|
const arg = try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size});
|
|
try argv.append(arg);
|
|
}
|
|
try argv.append("--no-entry"); // So lld doesn't look for _start.
|
|
try argv.append("--export-all");
|
|
}
|
|
try argv.appendSlice(&[_][]const u8{
|
|
"--allow-undefined",
|
|
"-o",
|
|
full_out_path,
|
|
});
|
|
|
|
if (target.os.tag == .wasi) {
|
|
const is_exe_or_dyn_lib = self.base.options.output_mode == .Exe or
|
|
(self.base.options.output_mode == .Lib and self.base.options.link_mode == .Dynamic);
|
|
if (is_exe_or_dyn_lib) {
|
|
const wasi_emulated_libs = self.base.options.wasi_emulated_libs;
|
|
for (wasi_emulated_libs) |crt_file| {
|
|
try argv.append(try comp.get_libc_crt_file(
|
|
arena,
|
|
wasi_libc.emulatedLibCRFileLibName(crt_file),
|
|
));
|
|
}
|
|
|
|
if (self.base.options.link_libc) {
|
|
try argv.append(try comp.get_libc_crt_file(
|
|
arena,
|
|
wasi_libc.execModelCrtFileFullName(self.base.options.wasi_exec_model),
|
|
));
|
|
try argv.append(try comp.get_libc_crt_file(arena, "libc.a"));
|
|
}
|
|
|
|
if (self.base.options.link_libcpp) {
|
|
try argv.append(comp.libcxx_static_lib.?.full_object_path);
|
|
try argv.append(comp.libcxxabi_static_lib.?.full_object_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Positional arguments to the linker such as object files.
|
|
try argv.appendSlice(self.base.options.objects);
|
|
|
|
for (comp.c_object_table.keys()) |key| {
|
|
try argv.append(key.status.success.object_path);
|
|
}
|
|
if (module_obj_path) |p| {
|
|
try argv.append(p);
|
|
}
|
|
|
|
if (self.base.options.output_mode != .Obj and
|
|
!self.base.options.skip_linker_dependencies and
|
|
!self.base.options.link_libc)
|
|
{
|
|
try argv.append(comp.libc_static_lib.?.full_object_path);
|
|
}
|
|
|
|
if (compiler_rt_path) |p| {
|
|
try argv.append(p);
|
|
}
|
|
|
|
if (self.base.options.verbose_link) {
|
|
// Skip over our own name so that the LLD linker name is the first argv item.
|
|
Compilation.dump_argv(argv.items[1..]);
|
|
}
|
|
|
|
// Sadly, we must run LLD as a child process because it does not behave
|
|
// properly as a library.
|
|
const child = try std.ChildProcess.init(argv.items, arena);
|
|
defer child.deinit();
|
|
|
|
if (comp.clang_passthrough_mode) {
|
|
child.stdin_behavior = .Inherit;
|
|
child.stdout_behavior = .Inherit;
|
|
child.stderr_behavior = .Inherit;
|
|
|
|
const term = child.spawnAndWait() catch |err| {
|
|
log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
|
|
return error.UnableToSpawnSelf;
|
|
};
|
|
switch (term) {
|
|
.Exited => |code| {
|
|
if (code != 0) {
|
|
// TODO https://github.com/ziglang/zig/issues/6342
|
|
std.process.exit(1);
|
|
}
|
|
},
|
|
else => std.process.abort(),
|
|
}
|
|
} else {
|
|
child.stdin_behavior = .Ignore;
|
|
child.stdout_behavior = .Ignore;
|
|
child.stderr_behavior = .Pipe;
|
|
|
|
try child.spawn();
|
|
|
|
const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
|
|
|
|
const term = child.wait() catch |err| {
|
|
log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
|
|
return error.UnableToSpawnSelf;
|
|
};
|
|
|
|
switch (term) {
|
|
.Exited => |code| {
|
|
if (code != 0) {
|
|
// TODO parse this output and surface with the Compilation API rather than
|
|
// directly outputting to stderr here.
|
|
std.debug.print("{s}", .{stderr});
|
|
return error.LLDReportedFailure;
|
|
}
|
|
},
|
|
else => {
|
|
log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
|
|
return error.LLDCrashed;
|
|
},
|
|
}
|
|
|
|
if (stderr.len != 0) {
|
|
log.warn("unexpected LLD stderr:\n{s}", .{stderr});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!self.base.options.disable_lld_caching) {
|
|
// Update the file with the digest. If it fails we can continue; it only
|
|
// means that the next invocation will have an unnecessary cache miss.
|
|
Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
|
|
log.warn("failed to save linking hash digest symlink: {s}", .{@errorName(err)});
|
|
};
|
|
// Again failure here only means an unnecessary cache miss.
|
|
man.writeManifest() catch |err| {
|
|
log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
|
|
};
|
|
// We hang on to this lock so that the output file path can be used without
|
|
// other processes clobbering it.
|
|
self.base.lock = man.toOwnedLock();
|
|
}
|
|
}
|
|
|
|
/// Get the current index of a given Decl in the function list
|
|
/// This will correctly provide the index, regardless whether the function is extern or not
|
|
/// TODO: we could maintain a hash map to potentially make this simpler
|
|
fn getFuncidx(self: Wasm, decl: *Module.Decl) ?u32 {
|
|
var offset: u32 = 0;
|
|
const slice = switch (decl.val.tag()) {
|
|
.function => blk: {
|
|
// when the target is a regular function, we have to calculate
|
|
// the offset of where the index starts
|
|
offset += self.getFuncIdxOffset();
|
|
break :blk self.funcs.items;
|
|
},
|
|
.extern_fn => self.ext_funcs.items,
|
|
else => return null,
|
|
};
|
|
return for (slice) |func, idx| {
|
|
if (func == decl) break @intCast(u32, offset + idx);
|
|
} else null;
|
|
}
|
|
|
|
/// Based on the size of `ext_funcs` returns the
|
|
/// offset of the function indices
|
|
fn getFuncIdxOffset(self: Wasm) u32 {
|
|
return @intCast(u32, self.ext_funcs.items.len);
|
|
}
|
|
|
|
fn reserveVecSectionHeader(file: fs.File) !u64 {
|
|
// section id + fixed leb contents size + fixed leb vector length
|
|
const header_size = 1 + 5 + 5;
|
|
// TODO: this should be a single lseek(2) call, but fs.File does not
|
|
// currently provide a way to do this.
|
|
try file.seekBy(header_size);
|
|
return (try file.getPos()) - header_size;
|
|
}
|
|
|
|
fn writeVecSectionHeader(file: fs.File, offset: u64, section: wasm.Section, size: u32, items: u32) !void {
|
|
var buf: [1 + 5 + 5]u8 = undefined;
|
|
buf[0] = @enumToInt(section);
|
|
leb.writeUnsignedFixed(5, buf[1..6], size);
|
|
leb.writeUnsignedFixed(5, buf[6..], items);
|
|
try file.pwriteAll(&buf, offset);
|
|
}
|