build: add option to export core as static c lib

This commit is contained in:
Arran Ireland
2025-07-11 17:17:52 +01:00
parent cc9d31a099
commit 612d3c08c5
10 changed files with 302 additions and 51 deletions

View File

@@ -5,6 +5,7 @@ const Optimize = std.builtin.OptimizeMode;
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
var builder = Builder.init(b); var builder = Builder.init(b);
builder.c();
builder.examples(); builder.examples();
builder.tests(); builder.tests();
} }
@@ -16,6 +17,7 @@ const Builder = struct {
const TestSuite = enum { integration, simulation }; const TestSuite = enum { integration, simulation };
b: *std.Build, b: *std.Build,
options: *std.Build.Step.Options,
target: Target, target: Target,
optimize: Optimize, optimize: Optimize,
@@ -37,11 +39,32 @@ const Builder = struct {
return .{ return .{
.b = b, .b = b,
.options = b.addOptions(),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}; };
} }
pub fn c(self: *Self) void {
const step = self.b.step("c", "Build core as a c library");
const static_lib = self.b.addLibrary(.{
.name = self.b.fmt("rtcore", .{}),
.root_module = self.module(.core),
.linkage = .static,
});
const exports = @import("core/exports.zig");
const write_files = self.b.addWriteFiles();
const header_path = write_files.add("rt_core.h", exports.generateHeader());
const install_header_file = self.b.addInstallHeaderFile(header_path, "rt_core.h");
const install_static_lib = self.b.addInstallArtifact(static_lib, .{});
install_header_file.step.dependOn(&write_files.step);
step.dependOn(&install_static_lib.step);
step.dependOn(&install_header_file.step);
}
pub fn examples(self: *Self) void { pub fn examples(self: *Self) void {
const step = self.b.step("example", "Run an example"); const step = self.b.step("example", "Run an example");
@@ -89,6 +112,7 @@ const Builder = struct {
.target = self.target, .target = self.target,
.optimize = self.optimize, .optimize = self.optimize,
}); });
step.dependOn(&self.b.addRunArtifact(compile).step); step.dependOn(&self.b.addRunArtifact(compile).step);
} }
@@ -142,6 +166,10 @@ const Builder = struct {
} }
fn addImport(self: *Self, compile: *std.Build.Step.Compile, comptime package: Package) void { fn addImport(self: *Self, compile: *std.Build.Step.Compile, comptime package: Package) void {
compile.root_module.addImport(@tagName(package), self.b.modules.getPtr(@tagName(package)).?.*); compile.root_module.addImport(@tagName(package), self.module(package));
}
fn module(self: *Self, comptime package: Package) *std.Build.Module {
return self.b.modules.getPtr(@tagName(package)).?.*;
} }
}; };

View File

@@ -1,7 +1,5 @@
const std = @import("std"); const std = @import("std");
// TODO: Add storage interface.
pub const Clock = struct { pub const Clock = struct {
const Self = @This(); const Self = @This();
@@ -15,5 +13,57 @@ pub const Clock = struct {
pub const Rng = std.Random; pub const Rng = std.Random;
pub const SimpleClock = struct {
pub const Callback = *const fn () callconv(.c) u64;
const Self = @This();
monotonicMicrosFn: Callback,
pub fn init(monotonicMicrosFn: Callback) Self {
return .{
.monotonicMicrosFn = monotonicMicrosFn,
};
}
pub fn monotonicMicros(ptr: *anyopaque) u64 {
const self: *Self = @ptrCast(@alignCast(ptr));
return self.monotonicMicrosFn();
}
pub fn clock(self: *Self) Clock {
return .{
.ptr = self,
.monotonicMicrosFn = monotonicMicros,
};
}
};
pub const SimpleRng = struct {
pub const Callback = *const fn (buf: [*]u8, length: usize) callconv(.c) void;
const Self = @This();
rngFillFn: Callback,
pub fn init(rngFillFn: Callback) Self {
return .{
.rngFillFn = rngFillFn,
};
}
pub fn rngFill(ptr: *anyopaque, buf: []u8) void {
const self: *Self = @ptrCast(@alignCast(ptr));
self.rngFillFn(buf.ptr, buf.len);
}
pub fn rng(self: *Self) Rng {
return .{
.ptr = self,
.fillFn = rngFill,
};
}
};
clock: Clock, clock: Clock,
rng: Rng, rng: Rng,

187
core/exports.zig Normal file
View File

@@ -0,0 +1,187 @@
//! This file is not part of the core module and therefore can contain non-freestanding code.
//! Be aware that this file will be imported into the build executable in order to generate the header file.
const builtin = @import("builtin");
const std = @import("std");
const lib = @import("lib.zig");
const Allocator = std.mem.Allocator;
const Gpa = std.heap.GeneralPurposeAllocator(.{});
const Self = @This();
var gpa: ?Gpa = null;
var allocator: ?Allocator = null;
var clock: lib.System.SimpleClock = undefined;
var rng: lib.System.SimpleRng = undefined;
var system: lib.System = undefined;
// Exported structs and functions.
pub const Error = enum(c_int) {
none = 0,
missing_allocator = 1,
out_of_memory = 2,
leaked_memory = 3,
unknown = 255,
};
pub fn libInit(
monotonicMicros: lib.System.SimpleClock.Callback,
rngFill: lib.System.SimpleRng.Callback,
) callconv(.c) Error {
if (allocator != null) return .missing_allocator;
gpa = Gpa{};
allocator = gpa.?.allocator();
clock = lib.System.SimpleClock.init(monotonicMicros);
rng = lib.System.SimpleRng.init(rngFill);
system = lib.System{
.clock = clock.clock(),
.rng = rng.rng(),
};
return .none;
}
pub fn libDeinit() callconv(.c) Error {
if (gpa == null or allocator == null) return .none;
if (gpa) |*g| {
if (g.deinit() == .leak) {
return .leaked_memory;
}
}
return .none;
}
pub fn makeNode(node_ptr: **anyopaque) callconv(.c) Error {
const ally = allocator orelse return .missing_allocator;
const node = ally.create(lib.Node) catch |e| return convertError(Allocator.Error, e);
errdefer ally.destroy(node);
node_ptr.* = node;
node.* = lib.Node.init(
ally,
&system,
null,
.{},
) catch |e| return convertError(lib.Node.Error, e);
return .none;
}
fn convertError(comptime E: type, e: E) Error {
if (E == lib.Node.Error) {
return switch (e) {
error.OutOfMemory => .out_of_memory,
else => .unknown,
};
}
return .unknown;
}
// Perform exports.
// This is currently also adding these symbols to the build executable.
// As far as I can tell it won't cause any issues and can probably be changed later.
comptime {
for (@typeInfo(@This()).@"struct".decls) |declaration| {
const field = @field(@This(), declaration.name);
const info = @typeInfo(@TypeOf(field));
if (info != .@"fn") continue;
if (std.mem.eql(u8, declaration.name, "generateHeader")) continue;
const function: *const anyopaque = @ptrCast(&field);
const export_options = std.builtin.ExportOptions{
.name = deriveName(declaration.name),
.linkage = .strong,
};
@export(function, export_options);
}
}
/// This needs to be public for use in the build c step.
pub fn generateHeader() []const u8 {
comptime var header: []const u8 =
\\#ifndef RT_CORE_H
\\#define RT_CORE_H
\\
\\#include <stddef.h>
\\#include <stdint.h>
\\
\\
;
inline for (@typeInfo(Self).@"struct".decls) |declaration| {
const field = @field(Self, declaration.name);
const info = @typeInfo(@TypeOf(field));
if (info != .@"fn") continue;
if (comptime std.mem.eql(u8, declaration.name, "generateHeader")) continue;
const function = info.@"fn";
const name = comptime deriveName(declaration.name);
const return_type = switch (function.return_type.?) {
Error => "int",
*anyopaque => "void*",
c_int => "int",
else => @typeName(function.return_type.?),
};
comptime var forward_declaration: []const u8 = return_type ++ " " ++ name ++ "(";
inline for (function.params, 0..) |param, i| {
const param_type = switch (param.type.?) {
lib.System.SimpleClock.Callback => "int64_t (*monotonic_micros)(void)",
lib.System.SimpleRng.Callback => "void (*rng_fill)(uint8_t* buf, size_t length)",
*anyopaque => "void*",
**anyopaque => "void**",
c_int => "int",
else => @typeName(param.type.?),
};
forward_declaration = forward_declaration ++ param_type;
if (i != function.params.len - 1) {
forward_declaration = forward_declaration ++ ", ";
}
}
forward_declaration = forward_declaration ++ ");\n";
header = header ++ forward_declaration;
}
header = header ++
\\
\\#endif
\\
;
return header;
}
fn deriveName(comptime name: []const u8) []const u8 {
var c_name: []const u8 = "rt_";
inline for (name) |char| {
if (std.ascii.isUpper(char)) {
c_name = c_name ++ .{ '_', std.ascii.toLower(char) };
} else {
c_name = c_name ++ .{char};
}
}
const arch = builtin.target.cpu.arch;
if (arch == .wasm32 or arch == .wasm64) {
return name;
} else {
return c_name;
}
}

View File

@@ -15,6 +15,9 @@ comptime {
const builtin = @import("builtin"); const builtin = @import("builtin");
const std = @import("std"); const std = @import("std");
// We import the file so that the exports actually run for this module.
_ = @import("exports.zig");
// Ensure unit tests are ran by referencing relevant files. // Ensure unit tests are ran by referencing relevant files.
if (builtin.is_test) { if (builtin.is_test) {
// Private files must be specifically enumerated. // Private files must be specifically enumerated.

View File

@@ -7,11 +7,10 @@ pub fn main() !void {
defer _ = gpa.deinit(); defer _ = gpa.deinit();
const ally = gpa.allocator(); const ally = gpa.allocator();
var clock = try io.os.Clock.init(); var clock = try io.system.Clock.init();
var rng = io.os.Rng.init();
var system = core.System{ var system = core.System{
.clock = clock.clock(), .clock = clock.clock(),
.rng = rng.rng(), .rng = std.crypto.random,
}; };
var node = try core.Node.init(ally, &system, null, .{}); var node = try core.Node.init(ally, &system, null, .{});

View File

@@ -1,2 +1,2 @@
pub const driver = @import("driver.zig"); pub const driver = @import("driver.zig");
pub const os = @import("os.zig"); pub const system = @import("system.zig");

View File

@@ -1,5 +0,0 @@
const core = @import("core");
const std = @import("std");
pub const Clock = @import("os/Clock.zig");
pub const Rng = @import("os/Rng.zig");

View File

@@ -1,26 +0,0 @@
const core = @import("core");
const std = @import("std");
const Timer = std.time.Timer;
const Self = @This();
timer: Timer,
pub fn init() Timer.Error!Self {
return .{
.timer = try Timer.start(),
};
}
pub fn monotonicMicros(ptr: *anyopaque) u64 {
const self: *Self = @ptrCast(@alignCast(ptr));
return self.timer.read();
}
pub fn clock(self: *Self) core.System.Clock {
return .{
.ptr = self,
.monotonicMicrosFn = monotonicMicros,
};
}

View File

@@ -1,13 +0,0 @@
const core = @import("core");
const std = @import("std");
const Self = @This();
pub fn init() Self {
return .{};
}
pub fn rng(self: *Self) core.System.Rng {
_ = self;
return std.crypto.random;
}

28
io/system.zig Normal file
View File

@@ -0,0 +1,28 @@
const core = @import("core");
const std = @import("std");
pub const Clock = struct {
const Timer = std.time.Timer;
const Self = @This();
timer: Timer,
pub fn init() Timer.Error!Self {
return .{
.timer = try Timer.start(),
};
}
pub fn monotonicMicros(ptr: *anyopaque) u64 {
const self: *Self = @ptrCast(@alignCast(ptr));
return self.timer.read();
}
pub fn clock(self: *Self) core.System.Clock {
return .{
.ptr = self,
.monotonicMicrosFn = monotonicMicros,
};
}
};