raylib-zig/generate.zig
2024-02-10 19:13:34 +02:00

530 lines
17 KiB
Zig

const std = @import("std");
const fs = std.fs;
const json = std.json;
const allocPrint = std.fmt.allocPrint;
const mapping = @import("type_mapping.zig");
const intermediate = @import("intermediate.zig");
pub const outputFile = "raylib.zig";
pub const injectFile = "inject.zig";
fn trim(s: []const u8) []const u8 {
return std.mem.trim(u8, s, &[_]u8{ ' ', '\t', '\n' });
}
pub fn main() !void {
std.log.info("generating raylib.zig ...", .{});
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
if (gpa.deinit() == .leak) {
std.log.err("memory leak detected", .{});
}
}
const allocator = gpa.allocator();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const bindingsData = try fs.cwd().readFileAlloc(arena.allocator(), intermediate.bindingsJSON, std.math.maxInt(usize));
const bindings = try json.parseFromSliceLeaky(mapping.Intermediate, arena.allocator(), bindingsData, .{
.ignore_unknown_fields = true,
});
var file = try fs.cwd().createFile(outputFile, .{});
defer file.close();
try file.writeAll("\n\n");
const inject = try Injections.load(arena.allocator());
try writeInjections(arena.allocator(), &file, inject);
try writeFunctions(arena.allocator(), &file, bindings, inject);
var h = try fs.cwd().createFile("marshal.h", .{});
defer h.close();
var c = try fs.cwd().createFile("marshal.c", .{});
defer c.close();
const raylib: mapping.CombinedRaylib = try mapping.CombinedRaylib.load(arena.allocator(), intermediate.jsonFiles);
try writeCFunctions(arena.allocator(), &h, &c, inject, raylib);
try writeStructs(arena.allocator(), &file, bindings, inject);
try writeEnums(arena.allocator(), &file, bindings, inject);
try writeDefines(arena.allocator(), &file, bindings, inject);
std.log.info("... done", .{});
}
const Injections = struct {
lines: []const []const u8,
symbols: []const []const u8,
pub fn load(allocator: std.mem.Allocator) !@This() {
var injectZigLines = std.ArrayList([]const u8).init(allocator);
var symbols = std.ArrayList([]const u8).init(allocator);
var file = try fs.cwd().openFile(injectFile, .{});
var reader = file.reader();
while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', std.math.maxInt(usize))) |line| {
if (std.mem.indexOf(u8, line, "pub const ")) |startIndex| {
if (std.mem.indexOf(u8, line, " = extern struct {")) |endIndex| {
const s = line["pub const ".len + startIndex .. endIndex];
try symbols.append(s);
std.log.debug("inject symbol: {s}", .{s});
} else if (std.mem.indexOf(u8, line, " = packed struct(u32) {")) |endIndex| {
const s = line["pub const ".len + startIndex .. endIndex];
try symbols.append(s);
std.log.debug("inject symbol: {s}", .{s});
}
}
if (std.mem.indexOf(u8, line, "pub fn ")) |startIndex| {
if (std.mem.indexOf(u8, line, "(")) |endIndex| {
const s = line["pub fn ".len + startIndex .. endIndex];
try symbols.append(s);
std.log.debug("inject symbol: {s}", .{s});
}
}
try injectZigLines.append(line);
}
return @This(){
.lines = try injectZigLines.toOwnedSlice(),
.symbols = try symbols.toOwnedSlice(),
};
}
pub fn containsSymbol(self: @This(), name: []const u8) bool {
for (self.symbols) |s| {
if (eql(s, name)) return true;
}
return false;
}
};
fn writeFunctions(
allocator: std.mem.Allocator,
file: *fs.File,
bindings: mapping.Intermediate,
inject: Injections,
) !void {
var buf: [51200]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
for (bindings.functions) |func| {
if (inject.containsSymbol(func.name)) continue;
defer fba.reset();
//--- signature -------------------------
const funcDescription: []const u8 = func.description orelse "";
try file.writeAll(
try allocPrint(
allocator,
"\n/// {s}\npub fn {s} (\n",
.{ funcDescription, func.name },
),
);
for (func.params) |param| {
if (param.description) |description| {
try file.writeAll(try allocPrint(
allocator,
"/// {s}\n",
.{description},
));
}
try file.writeAll(try allocPrint(
allocator,
"{s}: {s},\n",
.{ param.name, param.typ },
));
}
if (func.custom) {
try file.writeAll(
try allocPrint(
allocator,
") {s} {{\n",
.{func.returnType},
),
);
} else if (isPointer(func.returnType)) {
if (eql("u8", (stripType(func.returnType)))) {
try file.writeAll(
try allocPrint(
allocator,
") [*:0]const {s} {{\n",
.{stripType(func.returnType)},
),
);
} else {
try file.writeAll(
try allocPrint(
allocator,
") {s} {{\n",
.{func.returnType},
),
);
}
} else {
try file.writeAll(
try allocPrint(
allocator,
") {s} {{\n",
.{func.returnType},
),
);
}
const returnTypeIsVoid = eql(func.returnType, "void");
//--- body ------------------------------
if (isPointer(func.returnType)) {
try file.writeAll(try allocPrint(allocator, "return @ptrCast({s},\n", .{func.returnType}));
} else if (isPrimitiveOrPointer(func.returnType)) {
try file.writeAll("return ");
} else if (!returnTypeIsVoid) {
try file.writeAll(try allocPrint(allocator, "var out: {s} = undefined;\n", .{func.returnType}));
}
try file.writeAll(try allocPrint(allocator, "raylib.m{s}(\n", .{func.name}));
if (!isPrimitiveOrPointer(func.returnType)) {
if (bindings.containsStruct(stripType(func.returnType))) {
try file.writeAll(try allocPrint(allocator, "@ptrCast([*c]raylib.{s}, &out),\n", .{func.returnType}));
} else if (!returnTypeIsVoid) {
try file.writeAll(try allocPrint(allocator, "@ptrCast([*c]{s}, &out),\n", .{func.returnType}));
}
}
for (func.params) |param| {
if (isFunctionPointer(param.typ)) {
try file.writeAll(try allocPrint(allocator, "@ptrCast({s}),\n", .{param.name}));
} else if (bindings.containsStruct(stripType(param.typ)) and isPointer(param.typ)) {
try file.writeAll(try allocPrint(allocator, "@intToPtr([*c]raylib.{s}, @ptrToInt({s})),\n", .{ stripType(param.typ), param.name }));
} else if (bindings.containsEnum(param.typ)) {
try file.writeAll(try allocPrint(allocator, "@enumToInt({s}),\n", .{param.name}));
} else if (bindings.containsStruct(stripType(param.typ))) {
try file.writeAll(try allocPrint(allocator, "@intToPtr([*c]raylib.{s}, @ptrToInt(&{s})),\n", .{ stripType(param.typ), param.name }));
} else if (isPointer(param.typ)) {
if (std.mem.endsWith(u8, param.typ, "anyopaque")) {
try file.writeAll(try allocPrint(allocator, "{s},\n", .{param.name}));
} else if (isConst(param.typ)) {
try file.writeAll(try allocPrint(allocator, "@intToPtr([*c]const {s}, @ptrToInt({s})),\n", .{ stripType(param.typ), param.name }));
} else {
try file.writeAll(try allocPrint(allocator, "@ptrCast([*c]{s}, {s}),\n", .{ stripType(param.typ), param.name }));
}
} else {
try file.writeAll(try allocPrint(allocator, "{s},\n", .{param.name}));
}
}
if (isPointer(func.returnType)) {
try file.writeAll("),\n);\n");
} else {
try file.writeAll(");\n");
}
if (!isPrimitiveOrPointer(func.returnType) and !returnTypeIsVoid) {
try file.writeAll("return out;\n");
}
try file.writeAll("}\n");
}
std.log.info("generated functions", .{});
}
/// write: RETURN NAME(PARAMS...)
/// or: void NAME(RETURN*, PARAMS...)
fn writeCSignature(
allocator: std.mem.Allocator,
file: *fs.File,
func: mapping.RaylibFunction,
) !void {
const returnType = func.returnType;
//return directly
if (mapping.isPrimitiveOrPointer(returnType)) {
try file.writeAll(try allocPrint(allocator, "{s} m{s}(", .{ returnType, func.name }));
if (func.params == null or func.params.?.len == 0) {
try file.writeAll("void");
}
}
//wrap return type and put as first function parameter
else {
try file.writeAll(try allocPrint(allocator, "void m{s}({s} *out", .{ func.name, returnType }));
if (func.params != null and func.params.?.len > 0) {
try file.writeAll(", ");
}
}
if (func.params) |params| {
for (params, 0..) |param, i| {
const paramType = param.type;
if (mapping.isPrimitiveOrPointer(paramType) or isFunctionPointer(paramType)) {
try file.writeAll(try allocPrint(allocator, "{s} {s}", .{ paramType, param.name }));
} else {
try file.writeAll(try allocPrint(allocator, "{s} *{s}", .{ paramType, param.name }));
}
if (i < params.len - 1) {
try file.writeAll(", ");
}
}
}
try file.writeAll(")");
}
fn writeCFunctions(
allocator: std.mem.Allocator,
h: *fs.File,
c: *fs.File,
inject: Injections,
rl: mapping.CombinedRaylib,
) !void {
var hInject = try fs.cwd().openFile("inject.h", .{});
defer hInject.close();
try h.writeFileAll(hInject, .{});
var cInject = try fs.cwd().openFile("inject.c", .{});
defer cInject.close();
try c.writeFileAll(cInject, .{});
for (rl.functions.values()) |func| {
if (inject.containsSymbol(func.name)) continue;
//--- C-HEADER -----------------------------
try h.writeAll(try allocPrint(allocator, "// {s}\n", .{func.description}));
try writeCSignature(allocator, h, func);
try h.writeAll(";\n\n");
try writeCSignature(allocator, c, func);
try c.writeAll("\n{\n");
//--- C-IMPLEMENT -----------------------------
if (eql(func.returnType, "void")) {
try c.writeAll("\t");
} else if (mapping.isPrimitiveOrPointer(func.returnType)) {
try c.writeAll("\treturn ");
} else {
try c.writeAll("\t*out = ");
}
try c.writeAll(
try allocPrint(
allocator,
"{s}(",
.{func.name},
),
);
if (func.params) |params| {
for (params, 0..) |param, i| {
if (mapping.isPrimitiveOrPointer(param.type) or isFunctionPointer(param.type)) {
try c.writeAll(
try allocPrint(
allocator,
"{s}",
.{param.name},
),
);
} else {
try c.writeAll(
try allocPrint(
allocator,
"*{s}",
.{param.name},
),
);
}
if (i < params.len - 1) {
try c.writeAll(", ");
}
}
}
try c.writeAll(");\n}\n\n");
}
}
fn writeStructs(
allocator: std.mem.Allocator,
file: *fs.File,
bindings: mapping.Intermediate,
inject: Injections,
) !void {
var buf: [51200]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
for (bindings.structs) |s| {
if (inject.containsSymbol(s.name)) continue;
defer fba.reset();
try file.writeAll(
try allocPrint(
allocator,
"\n/// {?s}\npub const {s} = extern struct {{\n",
.{ s.description, s.name },
),
);
for (s.fields) |field| {
try file.writeAll(try allocPrint(allocator, "/// {?s}\n\t{s}: {s},\n", .{
field.description,
field.name,
field.typ,
}));
}
try file.writeAll("\n};\n");
}
std.log.info("generated structs", .{});
}
fn writeEnums(
allocator: std.mem.Allocator,
file: *fs.File,
bindings: mapping.Intermediate,
inject: Injections,
) !void {
var buf: [51200]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
for (bindings.enums) |e| {
if (inject.containsSymbol(e.name)) continue;
defer fba.reset();
try file.writeAll(
try allocPrint(
allocator,
"\n/// {?s}\npub const {s} = enum(i32) {{\n",
.{ e.description, e.name },
),
);
for (e.values) |value| {
try file.writeAll(try allocPrint(allocator, "/// {?s}\n{s} = {d},\n", .{
value.description,
value.name,
value.value,
}));
}
try file.writeAll("\n};\n");
}
std.log.info("generated enums", .{});
}
fn writeDefines(
allocator: std.mem.Allocator,
file: *fs.File,
bindings: mapping.Intermediate,
inject: Injections,
) !void {
var buf: [51200]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
for (bindings.defines) |d| {
if (inject.containsSymbol(d.name)) continue;
defer fba.reset();
try file.writeAll(
try allocPrint(
allocator,
"\n/// {?s}\npub const {s}: {s} = {s};\n",
.{ d.description, d.name, d.typ, d.value },
),
);
}
std.log.info("generated defines", .{});
}
fn writeInjections(
_: std.mem.Allocator,
file: *fs.File,
inject: Injections,
) !void {
var buf: [51200]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
for (inject.lines) |line| {
defer fba.reset();
try file.writeAll(try allocPrint(fba.allocator(), "{s}\n", .{line}));
}
std.log.info("written inject.zig", .{});
}
fn eql(a: []const u8, b: []const u8) bool {
return std.mem.eql(u8, a, b);
}
fn startsWith(haystack: []const u8, needle: []const u8) bool {
return std.mem.startsWith(u8, haystack, needle);
}
fn endsWith(haystack: []const u8, needle: []const u8) bool {
return std.mem.endsWith(u8, haystack, needle);
}
/// is pointer type
fn isPointer(z: []const u8) bool {
return pointerOffset(z) > 0;
}
fn isFunctionPointer(z: []const u8) bool {
return std.mem.indexOf(u8, z, "fn(") != null or std.mem.endsWith(u8, z, "Callback");
}
fn pointerOffset(z: []const u8) usize {
if (startsWith(z, "*")) return 1;
if (startsWith(z, "?*")) return 2;
if (startsWith(z, "[*]")) return 3;
if (startsWith(z, "?[*]")) return 4;
if (startsWith(z, "[*c]")) return 4;
if (startsWith(z, "[*:0]")) return 5;
if (startsWith(z, "?[*:0]")) return 6;
return 0;
}
fn isConst(z: []const u8) bool {
return startsWith(z[pointerOffset(z)..], "const ");
}
fn isVoid(z: []const u8) bool {
return eql(stripType(z), "void");
}
/// strips const and pointer annotations
/// *const TName -> TName
fn stripType(z: []const u8) []const u8 {
var name = z[pointerOffset(z)..];
name = if (startsWith(name, "const ")) name["const ".len..] else name;
return std.mem.trim(u8, name, " \t\n");
}
/// true if Zig type is primitive or a pointer to anything
/// this means we don't need to wrap it in a pointer
pub fn isPrimitiveOrPointer(z: []const u8) bool {
const primitiveTypes = std.ComptimeStringMap(void, .{
// .{ "void", {} }, // zig void is zero sized while C void is >= 1 byte
.{ "bool", {} },
.{ "u8", {} },
.{ "i8", {} },
.{ "i16", {} },
.{ "u16", {} },
.{ "i32", {} },
.{ "u32", {} },
.{ "i64", {} },
.{ "u64", {} },
.{ "f32", {} },
.{ "f64", {} },
});
return primitiveTypes.has(stripType(z)) or pointerOffset(z) > 0;
}