mirror of
https://github.com/Not-Nik/raylib-zig.git
synced 2025-09-09 03:57:29 +00:00
add web support using Emscripten
This commit is contained in:
parent
566a2860c7
commit
7a0d397baf
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,7 +1,9 @@
|
||||
zig-cache/
|
||||
zig-out/
|
||||
.idea/
|
||||
Project/*
|
||||
|
||||
#This is a file that is auto-generated to make a hack work
|
||||
**/web_emscripten_entry_point.zig
|
||||
libraylib.a
|
||||
raylib.h
|
||||
raymath.h
|
||||
|
12
README.md
12
README.md
@ -62,6 +62,18 @@ To build all available examples simply `zig build examples`. To list available e
|
||||
+ You can copy that folder anywhere you want and edit the source
|
||||
+ Run `zig build run` at any time to test your project
|
||||
|
||||
## Exporting for web
|
||||
To export your project for the web, first install emsdk.
|
||||
Once emsdk is installed, set it up by running
|
||||
|
||||
`emsdk install latest`
|
||||
|
||||
Find the folder where it's installed and run
|
||||
|
||||
`zig build webexport --sysroot [path to emsdk]/upstream/emscripten`
|
||||
|
||||
once that is finished, the exported project should be located at `zig-out/htmlout`
|
||||
|
||||
### When is the binding updated?
|
||||
|
||||
I plan on updating it every mayor release (2.5, 3.0, etc.). Keep in mind these are technically header files, so any implementation stuff should be updatable with some hacks on your side.
|
||||
|
202
build.zig
202
build.zig
@ -2,6 +2,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const rl = @This();
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Program = struct {
|
||||
name: []const u8,
|
||||
@ -14,7 +15,6 @@ pub fn link(b: *std.Build, exe: *std.Build.Step.Compile, target: std.zig.CrossTa
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
var art = raylib.artifact("raylib");
|
||||
|
||||
const target_os = exe.target.toTarget().os.tag;
|
||||
@ -43,6 +43,12 @@ pub fn link(b: *std.Build, exe: *std.Build.Step.Compile, target: std.zig.CrossTa
|
||||
exe.linkSystemLibrary("Xxf86vm");
|
||||
exe.linkSystemLibrary("Xcursor");
|
||||
},
|
||||
.emscripten, .wasi => {
|
||||
//when using emscripten,
|
||||
// the libries don't need to be linked
|
||||
// because emscripten is going
|
||||
// to do that later.
|
||||
},
|
||||
else => { // linux and possibly others
|
||||
exe.linkSystemLibrary("GL");
|
||||
exe.linkSystemLibrary("rt");
|
||||
@ -64,7 +70,15 @@ pub fn getArtifact(b: *std.Build, target: std.zig.CrossTarget, optimize: std.bui
|
||||
return raylib.artifact("raylib");
|
||||
}
|
||||
|
||||
pub fn getModule(b: *std.Build) *std.Build.Module {
|
||||
//TODO: make these not comptime
|
||||
pub fn getModule(b: *std.Build, comptime rl_path: []const u8) *std.Build.Module {
|
||||
if (b.modules.contains("raylib")) {
|
||||
return b.modules.get("raylib").?;
|
||||
}
|
||||
return b.addModule("raylib", .{ .source_file = .{ .path = rl_path ++ "/lib/raylib-zig.zig" } });
|
||||
}
|
||||
|
||||
pub fn getModuleInternal(b: *std.Build) *std.Build.Module {
|
||||
if (b.modules.contains("raylib")) {
|
||||
return b.modules.get("raylib").?;
|
||||
}
|
||||
@ -72,13 +86,18 @@ pub fn getModule(b: *std.Build) *std.Build.Module {
|
||||
}
|
||||
|
||||
pub const math = struct {
|
||||
pub fn getModule(b: *std.Build) *std.Build.Module {
|
||||
var raylib = rl.getModule(b);
|
||||
pub fn getModule(b: *std.Build, comptime rl_path: []const u8) *std.Build.Module {
|
||||
var raylib = rl.getModuleInternal(b);
|
||||
return b.addModule("raylib-math", .{ .source_file = .{ .path = rl_path ++ "/lib/raylib-zig-math.zig" }, .dependencies = &.{.{ .name = "raylib-zig", .module = raylib }} });
|
||||
}
|
||||
|
||||
fn getModuleInternal(b: *std.Build) *std.Build.Module {
|
||||
var raylib = rl.getModuleInternal(b);
|
||||
return b.addModule("raylib-math", .{ .source_file = .{ .path = "lib/raylib-zig-math.zig" }, .dependencies = &.{.{ .name = "raylib-zig", .module = raylib }} });
|
||||
}
|
||||
};
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
@ -149,19 +168,184 @@ pub fn build(b: *std.Build) void {
|
||||
const system_lib = b.option(bool, "system-raylib", "link to preinstalled raylib libraries") orelse false;
|
||||
_ = system_lib;
|
||||
|
||||
var raylib = rl.getModule(b);
|
||||
var raylib_math = rl.math.getModule(b);
|
||||
var raylib = rl.getModuleInternal(b);
|
||||
var raylib_math = rl.math.getModuleInternal(b);
|
||||
|
||||
for (examples) |ex| {
|
||||
const exe = b.addExecutable(.{ .name = ex.name, .root_source_file = .{ .path = ex.path }, .optimize = optimize, .target = target });
|
||||
//If compiling for emscripten, things need to be done COMPLETELY differently
|
||||
if (target.getOsTag() == .emscripten) {
|
||||
var build_step = try buildForEmscripten(b, ex.name, ex.path, target, optimize, raylib, raylib_math);
|
||||
var emrun_step = try emscriptenRunStep(b);
|
||||
emrun_step.step.dependOn(&build_step.step);
|
||||
var run_step = b.step(ex.name, ex.desc);
|
||||
run_step.dependOn(&emrun_step.step);
|
||||
//examples_step.dependOn(&build_step.step);
|
||||
continue;
|
||||
}
|
||||
|
||||
const exe = b.addExecutable(.{ .name = ex.name, .root_source_file = .{ .path = ex.path }, .optimize = optimize, .target = target });
|
||||
rl.link(b, exe, target, optimize);
|
||||
exe.addModule("raylib", raylib);
|
||||
exe.addModule("raylib-math", raylib_math);
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
const run_step = b.step(ex.name, ex.desc);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
examples_step.dependOn(&exe.step);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emscriptenRunStep(b: *std.Build) !*std.Build.Step.Run {
|
||||
//find emrun
|
||||
if (b.sysroot == null) {
|
||||
@panic("Pass '--sysroot \"[path to emsdk installation]/upstream/emscripten\"'");
|
||||
}
|
||||
//If compiling on windows , use emrun.bat
|
||||
const emrunExe = switch (builtin.os.tag) {
|
||||
.windows => "emrun.bat",
|
||||
else => "emrun",
|
||||
};
|
||||
var emrun_run_arg = try b.allocator.alloc(u8, b.sysroot.?.len + emrunExe.len + 1);
|
||||
defer b.allocator.free(emrun_run_arg);
|
||||
|
||||
emrun_run_arg = try std.fmt.bufPrint(emrun_run_arg, "{s}" ++ std.fs.path.sep_str ++ "{s}", .{ b.sysroot.?, emrunExe });
|
||||
|
||||
const run_cmd = b.addSystemCommand(&[_][]const u8{ emrun_run_arg, "zig-out" ++ std.fs.path.sep_str ++ "htmlout" ++ std.fs.path.sep_str ++ "index.html" });
|
||||
return run_cmd;
|
||||
}
|
||||
|
||||
pub fn buildForEmscripten(b: *std.Build, name: []const u8, root_source_file: []const u8, target: std.zig.CrossTarget, optimize: std.builtin.Mode, raylib: *std.Build.Module, raylib_math: *std.Build.Module) !*std.Build.Step.Run {
|
||||
const new_target = updateTargetForWeb(target);
|
||||
// Emscripten is completely unable to find Zig's entry point.
|
||||
// Solution: create a C compatible entry point in zig,
|
||||
// and then a C file that calls that entry point in the main method.
|
||||
// The C file is pre-written into the project,
|
||||
// However since the zig file needs to @import the main project,
|
||||
// it needs to be generated at compile time,
|
||||
// and it needs to be in the project's source.
|
||||
|
||||
//First, generate the file string
|
||||
const zig_entrypoint_format = "//This file is generated in order to allow web builds to start properly. It can safely be deleted.\nconst main = @import(\"{s}\");export fn web_emscripten_entry_point() c_int {{main.main() catch {{return -1;}};return 0;}}";
|
||||
const zig_entrypoint_buffer = try b.allocator.alloc(u8, zig_entrypoint_format.len + root_source_file.len + 1);
|
||||
const root_source_file_name = root_source_file[(lastIndexOf(root_source_file, '/') + 1)..];
|
||||
const zig_entrypoint_code = try std.fmt.bufPrint(zig_entrypoint_buffer, zig_entrypoint_format, .{root_source_file_name});
|
||||
const zig_entrypoint_dir = root_source_file[0..lastIndexOf(root_source_file, '/')];
|
||||
const zig_entrypoint_file = try b.allocator.alloc(u8, zig_entrypoint_dir.len + "/web_emscripten_entry_point.zig".len);
|
||||
_ = try std.fmt.bufPrint(zig_entrypoint_file, "{s}{s}", .{ zig_entrypoint_dir, "/web_emscripten_entry_point.zig" });
|
||||
|
||||
//TODO: find a way to create a C-compatible entrypoint without doing this
|
||||
const write_zig_entrypoint = b.addWriteFiles();
|
||||
write_zig_entrypoint.addBytesToSource(zig_entrypoint_code, zig_entrypoint_file);
|
||||
|
||||
//create the web back C file
|
||||
const webhack_c_file_step = b.addWriteFiles();
|
||||
const webhack_c_file = webhack_c_file_step.add("webhack.c", webhack_c);
|
||||
|
||||
//the project is built as a library and linked with everything later
|
||||
const exe_lib = b.addStaticLibrary(.{ .name = name, .root_source_file = .{ .path = zig_entrypoint_file }, .target = new_target, .optimize = optimize });
|
||||
exe_lib.addCSourceFile(.{ .file = webhack_c_file, .flags = &[_][]const u8{} });
|
||||
exe_lib.addModule("raylib", raylib);
|
||||
exe_lib.addModule("raylib-math", raylib_math);
|
||||
//link with libc since raylib isn't going to be linked in until later
|
||||
exe_lib.linkLibC();
|
||||
exe_lib.step.dependOn(&write_zig_entrypoint.step);
|
||||
exe_lib.step.dependOn(&webhack_c_file_step.step);
|
||||
//Emcc has to be the one that links the project, in order to make sure that the web libraries are correctly linked.
|
||||
if (b.sysroot == null) {
|
||||
@panic("Pass '--sysroot \"[path to emsdk installation]/upstream/emscripten\"'");
|
||||
}
|
||||
//If compiling on windows , use emcc.bat
|
||||
const emccExe = switch (builtin.os.tag) {
|
||||
.windows => "emcc.bat",
|
||||
else => "emcc",
|
||||
};
|
||||
var emcc_run_arg = try b.allocator.alloc(u8, b.sysroot.?.len + emccExe.len + 1);
|
||||
defer b.allocator.free(emcc_run_arg);
|
||||
|
||||
emcc_run_arg = try std.fmt.bufPrint(emcc_run_arg, "{s}" ++ std.fs.path.sep_str ++ "{s}", .{ b.sysroot.?, emccExe });
|
||||
|
||||
const raylib_artifact = getArtifact(b, target, optimize);
|
||||
//create the output directory because emcc can't do it.
|
||||
const mkdir_command = b.addSystemCommand(&[_][]const u8{ "mkdir", "-p", "zig-out" ++ std.fs.path.sep_str ++ "htmlout" ++ std.fs.path.sep_str });
|
||||
//Actually link everything together
|
||||
const emcc_command = b.addSystemCommand(&[_][]const u8{emcc_run_arg});
|
||||
emcc_command.addFileArg(raylib_artifact.getEmittedBin());
|
||||
emcc_command.addFileArg(exe_lib.getEmittedBin());
|
||||
//TODO: see if there is a way to properly integrate the output directory into zig's cache
|
||||
// LATER: The way is by zig's writeFile creates place in the cache that can then be used to place emcc's output
|
||||
// TODO: investigate why "-sSTANDALONE_WASM" completely freezes the page once the program loads and starts.
|
||||
emcc_command.addArgs(&[_][]const u8{ "-o", "zig-out" ++ std.fs.path.sep_str ++ "htmlout" ++ std.fs.path.sep_str ++ "index.html", "-sFULL-ES3=1", "-sUSE_GLFW=3", "-sASYNCIFY", "-O3" });
|
||||
emcc_command.step.dependOn(&exe_lib.step);
|
||||
emcc_command.step.dependOn(&raylib_artifact.step);
|
||||
emcc_command.step.dependOn(&mkdir_command.step);
|
||||
|
||||
return emcc_command;
|
||||
}
|
||||
|
||||
//TODO: see if zig's standard library already has somehing like this
|
||||
fn lastIndexOf(string: []const u8, character: u8) usize {
|
||||
//interestingly, zig has no nice way of iterating a slice backwards
|
||||
for (0..string.len) |i| {
|
||||
const index = string.len - i - 1;
|
||||
if (string[index] == character) return index;
|
||||
}
|
||||
return string.len - 1;
|
||||
}
|
||||
fn updateTargetForWeb(target: std.zig.CrossTarget) std.zig.CrossTarget {
|
||||
//zig building to emscripten doesn't really work,
|
||||
// as the standard library doesn't compile for some reason
|
||||
// So build to wasi instead.
|
||||
return std.zig.CrossTarget{
|
||||
.cpu_arch = target.cpu_arch,
|
||||
.cpu_model = target.cpu_model,
|
||||
.cpu_features_add = target.cpu_features_add,
|
||||
.cpu_features_sub = target.cpu_features_sub,
|
||||
.os_tag = .wasi,
|
||||
.os_version_min = target.os_version_min,
|
||||
.os_version_max = target.os_version_max,
|
||||
.glibc_version = target.glibc_version,
|
||||
.abi = target.abi,
|
||||
.dynamic_linker = target.dynamic_linker,
|
||||
.ofmt = target.ofmt,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn webExport(b: *std.Build, root_source_file: []const u8, comptime rl_path: []const u8) !*std.Build.Step {
|
||||
// EXPORTING to WEB, the only reasonable output is emscripten ReleaseSafe.
|
||||
// (Safe because since when was performance the main goal of the internet?)
|
||||
//Note: the name doesn't matter, it's only used as the file name of a temporary object, so it's just called "project"
|
||||
const target = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-emscripten" });
|
||||
const optimize = std.builtin.OptimizeMode.ReleaseSafe;
|
||||
var raylib = rl.getModule(b, rl_path);
|
||||
var raylib_math = rl.math.getModule(b, rl_path);
|
||||
const build_step = try buildForEmscripten(b, "project", root_source_file, target, optimize, raylib, raylib_math);
|
||||
const run_step = try emscriptenRunStep(b);
|
||||
run_step.step.dependOn(&build_step.step);
|
||||
const run_step_arg = b.step("run", "This only exists because the run step for web exports is different");
|
||||
run_step_arg.dependOn(&run_step.step);
|
||||
return &build_step.step;
|
||||
}
|
||||
const webhack_c =
|
||||
\\ // Emscripten is completely unable to find Zig's entry point.
|
||||
\\ // Solution: create a C compatible entry point in zig,
|
||||
\\ // and then a C file that calls that entry point in the main method
|
||||
\\ // This is that C file.
|
||||
\\ //The entry point found in the zig project
|
||||
\\ extern int web_emscripten_entry_point();
|
||||
\\ //this is the method that emscripten calls automatically when the page loads
|
||||
\\ int main(int argc, char** argv)
|
||||
\\ {
|
||||
\\ //TODO: possibly pass arguments into zig?
|
||||
\\ return web_emscripten_entry_point();
|
||||
\\ }
|
||||
\\ // For some reason zig adds '__stack_chk_guard', '__stack_chk_fail', and 'errno',
|
||||
\\ // which emscripten doesn't actually support.
|
||||
\\ // Seems that zig ignores disabling stack checking,
|
||||
\\ // and I honestly don't know why emscripten doesn't have errno.
|
||||
\\ // there's a solution, although it's not a good one...
|
||||
\\ #include <stdint.h>
|
||||
\\ uintptr_t __stack_chk_guard;
|
||||
\\ //Yes, this means that if there is a buffer overflow, nobody is going to know about it.
|
||||
\\ // However, zig is pretty safe from those, so don't worry about it too much.
|
||||
\\ void __stack_chk_fail(void){}
|
||||
\\ int errno;
|
||||
;
|
||||
|
@ -1296,7 +1296,7 @@ pub fn loadCodepoints(text: [:0]const u8) []i32 {
|
||||
}
|
||||
|
||||
pub fn textFormat(text: [:0]const u8, args: anytype) [:0]const u8 {
|
||||
return std.mem.span(@call(.{}, cdef.TextFormat, .{@as([*c]const u8, @ptrCast(text))} ++ args));
|
||||
return std.mem.span(@call(.auto, cdef.TextFormat, .{@as([*c]const u8, @ptrCast(text))} ++ args));
|
||||
}
|
||||
|
||||
pub fn textSplit(text: [:0]const u8, delimiter: u8) [][:0]const u8 {
|
||||
|
@ -12,24 +12,50 @@ echo "generating project files..."
|
||||
echo 'const std = @import("std");
|
||||
const rl = @import("raylib-zig/build.zig");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const web_export = argsContainsWebexport(b.allocator);
|
||||
if (!web_export) {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
var raylib = rl.getModule(b);
|
||||
var raylib_math = rl.math.getModule(b);
|
||||
var raylib = rl.getModule(b, "raylib-zig");
|
||||
var raylib_math = rl.math.getModule(b, "raylib-zig");
|
||||
|
||||
const exe = b.addExecutable(.{ .name = "'$PROJECT_NAME'", .root_source_file = .{ .path = "src/main.zig" }, .optimize = optimize, .target = target });
|
||||
|
||||
rl.link(b, exe, target, optimize);
|
||||
exe.addModule("raylib", raylib);
|
||||
exe.addModule("raylib-math", raylib_math);
|
||||
const exe = b.addExecutable(.{ .name = "'$PROJECT_NAME'", .root_source_file = .{ .path = "src/main.zig" }, .optimize = optimize, .target = target });
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
const run_step = b.step("run", "Run '$PROJECT_NAME'");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
rl.link(b, exe, target, optimize);
|
||||
exe.addModule("raylib", raylib);
|
||||
exe.addModule("raylib-math", raylib_math);
|
||||
|
||||
b.installArtifact(exe);
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
const run_step = b.step("run", "Run '$PROJECT_NAME'");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
b.installArtifact(exe);
|
||||
}
|
||||
|
||||
//web exports are completely separate, due to the amount of hackery required.
|
||||
const export_step = b.step("webexport", "Export '$PROJECT_NAME' for the web");
|
||||
if (web_export) {
|
||||
export_step.dependOn(try rl.webExport(b, "src/main.zig", "raylib-zig"));
|
||||
}
|
||||
|
||||
// Building for web requires a --sysroot [emscripten and stuff]
|
||||
// But asking for that for all builds is not a good user experience
|
||||
// So it will only actually set up the build scripts if the web export is actually going to happen.
|
||||
}
|
||||
|
||||
fn argsContainsWebexport(allocator: std.mem.Allocator) bool {
|
||||
const args = std.process.argsAlloc(allocator) catch {
|
||||
return false;
|
||||
};
|
||||
defer allocator.free(args);
|
||||
for (args) |arg| {
|
||||
if (std.mem.eql(u8, "webexport", arg)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
' >> build.zig
|
||||
|
||||
|
28
webbuildhack.c
Normal file
28
webbuildhack.c
Normal file
@ -0,0 +1,28 @@
|
||||
// Emscripten is completely unable to find Zig's entry point.
|
||||
// Solution: create a C compatible entry point in zig,
|
||||
// and then a C file that calls that entry point in the main method
|
||||
// This is that C file.
|
||||
|
||||
//The entry point found in the zig project
|
||||
extern int web_emscripten_entry_point();
|
||||
//this is the method that emscripten calls automatically when the page loads
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
//TODO: possibly pass arguments into zig?
|
||||
return web_emscripten_entry_point();
|
||||
}
|
||||
|
||||
// For some reason zig adds '__stack_chk_guard', '__stack_chk_fail', and 'errno',
|
||||
// which emscripten doesn't actually support.
|
||||
// Seems that zig ignores disabling stack checking,
|
||||
// and I honestly don't know why emscripten doesn't have errno.
|
||||
// there's a solution, although it's not a good one...
|
||||
#include <stdint.h>
|
||||
|
||||
uintptr_t __stack_chk_guard;
|
||||
|
||||
//Yes, this means that if there is a buffer overflow, nobody is going to know about it.
|
||||
// However, zig is pretty safe from those, so don't worry about it too much.
|
||||
void __stack_chk_fail(void){}
|
||||
|
||||
int errno;
|
Loading…
x
Reference in New Issue
Block a user