Skip to content

wasm-linker: implement -fno-entry and correctly pass --shared and --pie when given #17815

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 5 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
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
8 changes: 4 additions & 4 deletions doc/langref.html.in
Original file line number Diff line number Diff line change
Expand Up @@ -11336,12 +11336,12 @@ all your base are belong to us{#end_shell_samp#}
{#header_open|WebAssembly#}
<p>Zig supports building for WebAssembly out of the box.</p>
{#header_open|Freestanding#}
<p>For host environments like the web browser and nodejs, build as a dynamic library using the freestanding
<p>For host environments like the web browser and nodejs, build as an executable using the freestanding
OS target. Here's an example of running Zig code compiled to WebAssembly with nodejs.</p>
{#code_begin|lib|math#}
{#code_begin|exe|math#}
{#target_wasm#}
{#link_mode_dynamic#}
{#additonal_option|-rdynamic#}
{#additonal_option|-fno-entry#}
{#additonal_option|--export=add#}
extern fn print(i32) void;

export fn add(a: i32, b: i32) void {
Expand Down
25 changes: 21 additions & 4 deletions lib/std/Build/Step/Compile.zig
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ dll_export_fns: ?bool = null,

subsystem: ?std.Target.SubSystem = null,

entry_symbol_name: ?[]const u8 = null,
/// How the linker must handle the entry point of the executable.
entry: Entry = .default,

/// List of symbols forced as undefined in the symbol table
/// thus forcing their resolution by the linker.
Expand Down Expand Up @@ -304,6 +305,18 @@ const FrameworkLinkInfo = struct {
weak: bool = false,
};

const Entry = union(enum) {
/// Let the compiler decide whether to make an entry point and what to name
/// it.
default,
/// The executable will have no entry point.
disabled,
/// The executable will have an entry point with the default symbol name.
enabled,
/// The executable will have an entry point with the specified symbol name.
symbol_name: []const u8,
};

pub const IncludeDir = union(enum) {
path: LazyPath,
path_system: LazyPath,
Expand Down Expand Up @@ -1418,9 +1431,13 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
try zig_args.append(try std.fmt.allocPrint(b.allocator, "-ofmt={s}", .{@tagName(ofmt)}));
}

if (self.entry_symbol_name) |entry| {
try zig_args.append("--entry");
try zig_args.append(entry);
switch (self.entry) {
.default => {},
.disabled => try zig_args.append("-fno-entry"),
.enabled => try zig_args.append("-fentry"),
.symbol_name => |entry_name| {
try zig_args.append(try std.fmt.allocPrint(b.allocator, "-fentry={s}", .{entry_name}));
},
}

{
Expand Down
8 changes: 6 additions & 2 deletions lib/std/start.zig
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ comptime {
.reactor => "_initialize",
.command => "_start",
};
if (!@hasDecl(root, wasm_start_sym)) {
if (!@hasDecl(root, wasm_start_sym) and @hasDecl(root, "main")) {
// Only call main when defined. For WebAssembly it's allowed to pass `-fno-entry` in which
// case it's not required to provide an entrypoint such as main.
@export(wasi_start, .{ .name = wasm_start_sym });
}
} else if (native_arch.isWasm() and native_os == .freestanding) {
if (!@hasDecl(root, start_sym_name)) @export(wasm_freestanding_start, .{ .name = start_sym_name });
// Only call main when defined. For WebAssembly it's allowed to pass `-fno-entry` in which
// case it's not required to provide an entrypoint such as main.
if (!@hasDecl(root, start_sym_name) and @hasDecl(root, "main")) @export(wasm_freestanding_start, .{ .name = start_sym_name });
} else if (native_os != .other and native_os != .freestanding) {
if (!@hasDecl(root, start_sym_name)) @export(_start, .{ .name = start_sym_name });
}
Expand Down
33 changes: 12 additions & 21 deletions src/link/Wasm.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2817,15 +2817,11 @@ fn setupExports(wasm: *Wasm) !void {
}

fn setupStart(wasm: *Wasm) !void {
const entry_name = wasm.base.options.entry orelse "_start";
// do not export entry point if user set none or no default was set.
const entry_name = wasm.base.options.entry orelse return;

const symbol_loc = wasm.findGlobalSymbol(entry_name) orelse {
if (wasm.base.options.output_mode == .Exe) {
if (wasm.base.options.wasi_exec_model == .reactor) return; // Not required for reactors
} else {
return; // No entry point needed for non-executable wasm files
}
log.err("Entry symbol '{s}' missing", .{entry_name});
log.err("Entry symbol '{s}' missing, use '-fno-entry' to suppress", .{entry_name});
return error.MissingSymbol;
};

Expand Down Expand Up @@ -4535,6 +4531,8 @@ fn linkWithLLD(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) !
if (wasm.base.options.entry) |entry| {
try argv.append("--entry");
try argv.append(entry);
} else {
try argv.append("--no-entry");
}

// Increase the default stack size to a more reasonable value of 1MB instead of
Expand All @@ -4544,24 +4542,17 @@ fn linkWithLLD(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) !
const arg = try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size});
try argv.append(arg);

if (wasm.base.options.output_mode == .Exe) {
if (wasm.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.
// If rdynamic is true, it will already be appended, so only verify if the user did not specify
// the flag in which case, we ensure `--export-dynamic` is called.
if (!wasm.base.options.rdynamic) {
try argv.append("--export-dynamic");
}
}
} else if (wasm.base.options.entry == null) {
try argv.append("--no-entry"); // So lld doesn't look for _start.
}
if (wasm.base.options.import_symbols) {
try argv.append("--allow-undefined");
}

if (wasm.base.options.output_mode == .Lib and wasm.base.options.link_mode == .Dynamic) {
try argv.append("--shared");
}
if (wasm.base.options.pie) {
try argv.append("--pie");
}

// XXX - TODO: add when wasm-ld supports --build-id.
// if (wasm.base.options.build_id) {
// try argv.append("--build-id=tree");
Expand Down
57 changes: 54 additions & 3 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,9 @@ const usage_build_generic =
\\ --dynamic-linker [path] Set the dynamic interpreter path (usually ld.so)
\\ --sysroot [path] Set the system root directory (usually /)
\\ --version [ver] Dynamic library semver
\\ --entry [name] Set the entrypoint symbol name
\\ -fentry Enable entry point with default symbol name
\\ -fentry=[name] Override the entry point symbol name
\\ -fno-entry Do not output any entry point
\\ --force_undefined [name] Specify the symbol must be defined for the link to succeed
\\ -fsoname[=name] Override the default SONAME value
\\ -fno-soname Disable emitting a SONAME
Expand Down Expand Up @@ -835,6 +837,7 @@ fn buildOutputType(
var linker_import_symbols: bool = false;
var linker_import_table: bool = false;
var linker_export_table: bool = false;
var linker_force_entry: ?bool = null;
var linker_initial_memory: ?u64 = null;
var linker_max_memory: ?u64 = null;
var linker_shared_memory: bool = false;
Expand Down Expand Up @@ -1071,8 +1074,8 @@ fn buildOutputType(
subsystem = try parseSubSystem(args_iter.nextOrFatal());
} else if (mem.eql(u8, arg, "-O")) {
optimize_mode_string = args_iter.nextOrFatal();
} else if (mem.eql(u8, arg, "--entry")) {
entry = args_iter.nextOrFatal();
} else if (mem.startsWith(u8, arg, "-fentry=")) {
entry = arg["-fentry=".len..];
} else if (mem.eql(u8, arg, "--force_undefined")) {
try force_undefined_symbols.put(gpa, args_iter.nextOrFatal(), {});
} else if (mem.eql(u8, arg, "--stack")) {
Expand Down Expand Up @@ -1503,6 +1506,10 @@ fn buildOutputType(
}
} else if (mem.eql(u8, arg, "--import-memory")) {
linker_import_memory = true;
} else if (mem.eql(u8, arg, "-fentry")) {
linker_force_entry = true;
} else if (mem.eql(u8, arg, "-fno-entry")) {
linker_force_entry = false;
} else if (mem.eql(u8, arg, "--export-memory")) {
linker_export_memory = true;
} else if (mem.eql(u8, arg, "--import-symbols")) {
Expand Down Expand Up @@ -2134,6 +2141,8 @@ fn buildOutputType(
linker_import_table = true;
} else if (mem.eql(u8, arg, "--export-table")) {
linker_export_table = true;
} else if (mem.eql(u8, arg, "--no-entry")) {
linker_force_entry = false;
} else if (mem.eql(u8, arg, "--initial-memory")) {
const next_arg = linker_args_it.nextOrFatal();
linker_initial_memory = std.fmt.parseUnsigned(u32, eatIntPrefix(next_arg, 16), 16) catch |err| {
Expand Down Expand Up @@ -2596,6 +2605,23 @@ fn buildOutputType(
link_libcpp = true;
}

if (linker_force_entry) |force| {
if (!force) {
entry = null;
} else if (entry == null and output_mode == .Exe) {
entry = switch (target_info.target.ofmt) {
.coff => "wWinMainCRTStartup",
.macho => "_main",
.elf, .plan9 => "_start",
.wasm => defaultWasmEntryName(wasi_exec_model),
else => |tag| fatal("No default entry point available for output format {s}", .{@tagName(tag)}),
};
}
} else if (entry == null and target_info.target.isWasm() and output_mode == .Exe) {
// For WebAssembly the compiler defaults to setting the entry name when no flags are set.
entry = defaultWasmEntryName(wasi_exec_model);
}

if (target_info.target.ofmt == .coff) {
// Now that we know the target supports resources,
// we can add the res files as link objects.
Expand All @@ -2618,6 +2644,23 @@ fn buildOutputType(
if (single_threaded == null) {
single_threaded = true;
}
if (link_mode) |mode| {
if (mode == .Dynamic) {
if (linker_export_memory != null and linker_export_memory.?) {
fatal("flags '-dynamic' and '--export-memory' are incompatible", .{});
}
// User did not supply `--export-memory` which is incompatible with -dynamic, therefore
// set the flag to false to ensure it does not get enabled by default.
linker_export_memory = false;
}
}
if (wasi_exec_model != null and wasi_exec_model.? == .reactor) {
if (entry) |entry_name| {
if (!mem.eql(u8, "_initialize", entry_name)) {
fatal("the entry symbol of the reactor model must be '_initialize', but found '{s}'", .{entry_name});
}
}
}
if (linker_shared_memory) {
if (output_mode == .Obj) {
fatal("shared memory is not allowed in object files", .{});
Expand Down Expand Up @@ -7214,3 +7257,11 @@ fn createDependenciesModule(
try main_mod.deps.put(arena, "@dependencies", deps_mod);
return deps_mod;
}

fn defaultWasmEntryName(exec_model: ?std.builtin.WasiExecModel) []const u8 {
const model = exec_model orelse .command;
if (model == .reactor) {
return "_initialize";
}
return "_start";
}
4 changes: 2 additions & 2 deletions test/link/elf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ fn testEntryPoint(b: *Build, opts: Options) *Step {
const exe = addExecutable(b, "main", opts);
exe.addObject(a_o);
exe.addObject(b_o);
exe.entry_symbol_name = "foo";
exe.entry = .{ .symbol_name = "foo" };

const check = exe.checkObject();
check.checkStart();
Expand All @@ -667,7 +667,7 @@ fn testEntryPoint(b: *Build, opts: Options) *Step {
const exe = addExecutable(b, "other", opts);
exe.addObject(a_o);
exe.addObject(b_o);
exe.entry_symbol_name = "bar";
exe.entry = .{ .symbol_name = "bar" };

const check = exe.checkObject();
check.checkStart();
Expand Down
2 changes: 1 addition & 1 deletion test/link/macho/entry/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fn add(b: *std.Build, test_step: *std.Build.Step, optimize: std.builtin.Optimize
});
exe.addCSourceFile(.{ .file = .{ .path = "main.c" }, .flags = &.{} });
exe.linkLibC();
exe.entry_symbol_name = "_non_main";
exe.entry = .{ .symbol_name = "_non_main" };

const check_exe = exe.checkObject();

Expand Down
2 changes: 1 addition & 1 deletion test/link/macho/entry_in_dylib/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fn add(b: *std.Build, test_step: *std.Build.Step, optimize: std.builtin.Optimize
exe.addCSourceFile(.{ .file = .{ .path = "main.c" }, .flags = &.{} });
exe.linkLibrary(lib);
exe.linkLibC();
exe.entry_symbol_name = "_bootstrap";
exe.entry = .{ .symbol_name = "_bootstrap" };
exe.forceUndefinedSymbol("_my_main");

const check_exe = exe.checkObject();
Expand Down
3 changes: 2 additions & 1 deletion test/link/wasm/archive/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ pub fn build(b: *std.Build) void {
fn add(b: *std.Build, test_step: *std.Build.Step, optimize: std.builtin.OptimizeMode) void {
// The code in question will pull-in compiler-rt,
// and therefore link with its archive file.
const lib = b.addSharedLibrary(.{
const lib = b.addExecutable(.{
.name = "main",
.root_source_file = .{ .path = "main.zig" },
.optimize = optimize,
.target = .{ .cpu_arch = .wasm32, .os_tag = .freestanding },
});
lib.entry = .disabled;
lib.use_llvm = false;
lib.use_lld = false;
lib.strip = false;
Expand Down
3 changes: 2 additions & 1 deletion test/link/wasm/basic-features/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub const requires_stage2 = true;

pub fn build(b: *std.Build) void {
// Library with explicitly set cpu features
const lib = b.addSharedLibrary(.{
const lib = b.addExecutable(.{
.name = "lib",
.root_source_file = .{ .path = "main.zig" },
.optimize = .Debug,
Expand All @@ -15,6 +15,7 @@ pub fn build(b: *std.Build) void {
.os_tag = .freestanding,
},
});
lib.entry = .disabled;
lib.use_llvm = false;
lib.use_lld = false;

Expand Down
6 changes: 4 additions & 2 deletions test/link/wasm/bss/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ pub fn build(b: *std.Build) void {

fn add(b: *std.Build, test_step: *std.Build.Step, optimize_mode: std.builtin.OptimizeMode, is_safe: bool) void {
{
const lib = b.addSharedLibrary(.{
const lib = b.addExecutable(.{
.name = "lib",
.root_source_file = .{ .path = "lib.zig" },
.target = .{ .cpu_arch = .wasm32, .os_tag = .freestanding },
.optimize = optimize_mode,
});
lib.entry = .disabled;
lib.use_llvm = false;
lib.use_lld = false;
lib.strip = false;
Expand Down Expand Up @@ -60,12 +61,13 @@ fn add(b: *std.Build, test_step: *std.Build.Step, optimize_mode: std.builtin.Opt

// verify zero'd declaration is stored in bss for all optimization modes.
{
const lib = b.addSharedLibrary(.{
const lib = b.addExecutable(.{
.name = "lib",
.root_source_file = .{ .path = "lib2.zig" },
.target = .{ .cpu_arch = .wasm32, .os_tag = .freestanding },
.optimize = optimize_mode,
});
lib.entry = .disabled;
lib.use_llvm = false;
lib.use_lld = false;
lib.strip = false;
Expand Down
3 changes: 2 additions & 1 deletion test/link/wasm/export-data/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ pub fn build(b: *std.Build) void {
return;
}

const lib = b.addSharedLibrary(.{
const lib = b.addExecutable(.{
.name = "lib",
.root_source_file = .{ .path = "lib.zig" },
.optimize = .ReleaseSafe, // to make the output deterministic in address positions
.target = .{ .cpu_arch = .wasm32, .os_tag = .freestanding },
});
lib.entry = .disabled;
lib.use_lld = false;
lib.export_symbol_names = &.{ "foo", "bar" };
lib.global_base = 0; // put data section at address 0 to make data symbols easier to parse
Expand Down
Loading