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

692 lines
22 KiB
Zig

//! Strategy:
//! 1. generate raylib JSONs
//! 2. combine in a union RaylibJson
//! 3. convert to intermediate representation
//! 4. read adjusted intermediate JSONs
//! 5. generate raylib.zig // wrap paramters and pass them to marshall versions of the raylib functions
//! 6. generate marshall.c // unwrap parameters from zig function and call the actual raylib function
//! 7. generate marshall.h // C signatures for all marshalled functions
const std = @import("std");
const json = std.json;
const memoryConstrain: usize = 1024 * 1024 * 1024; // 1 GiB
const Allocator = std.mem.Allocator;
const fmt = std.fmt.allocPrint;
const talloc = std.testing.allocator;
const expect = std.testing.expect;
const expectEqualStrings = std.testing.expectEqualStrings;
//--- Intermediate Format -------------------------------------------------------------------------
pub const Intermediate = struct {
functions: []Function,
enums: []Enum,
structs: []Struct,
defines: []Define,
pub fn loadCustoms(allocator: Allocator, jsonFile: []const u8) !@This() {
var enums = std.ArrayList(Enum).init(allocator);
var structs = std.ArrayList(Struct).init(allocator);
var functions = std.ArrayList(Function).init(allocator);
var defines = std.ArrayList(Define).init(allocator);
const jsonData = try std.fs.cwd().readFileAlloc(allocator, jsonFile, memoryConstrain);
const bindingJson = try json.parseFromSliceLeaky(Intermediate, allocator, jsonData, .{
.ignore_unknown_fields = true,
});
for (bindingJson.enums) |t| {
if (t.custom) {
try enums.append(t);
}
}
for (bindingJson.structs) |t| {
if (t.custom) {
try structs.append(t);
}
}
for (bindingJson.functions) |f| {
if (f.custom) {
try functions.append(f);
}
}
for (bindingJson.defines) |f| {
if (f.custom) {
try defines.append(f);
}
}
return @This(){
.enums = try enums.toOwnedSlice(),
.structs = try structs.toOwnedSlice(),
.functions = try functions.toOwnedSlice(),
.defines = try defines.toOwnedSlice(),
};
}
pub fn addNonCustom(self: *@This(), allocator: Allocator, rlJson: CombinedRaylib) !void {
var enums = std.ArrayList(Enum).init(allocator);
try enums.appendSlice(self.enums);
var structs = std.ArrayList(Struct).init(allocator);
try structs.appendSlice(self.structs);
var functions = std.ArrayList(Function).init(allocator);
try functions.appendSlice(self.functions);
var defines = std.ArrayList(Define).init(allocator);
try defines.appendSlice(self.defines);
outer: for (rlJson.defines.values(), 0..) |d, i| {
for (defines.items) |added| {
if (eql(added.name, d.name)) {
std.log.debug("{s} is customized", .{d.name});
continue :outer;
}
}
const define = parseRaylibDefine(allocator, d) orelse continue :outer;
if (i < defines.items.len) {
try defines.insert(i, define);
} else {
try defines.append(define);
}
}
outer: for (rlJson.enums.values(), 0..) |e, i| {
const name = if (alias.get(e.name)) |n| n else e.name;
for (enums.items) |added| {
if (eql(added.name, name)) {
std.log.debug("{s} is customized", .{name});
continue :outer;
}
}
if (i < enums.items.len) {
try enums.insert(i, try parseRaylibEnum(allocator, e));
} else {
try enums.append(try parseRaylibEnum(allocator, e));
}
}
outer: for (rlJson.structs.values(), 0..) |s, i| {
const name = if (alias.get(s.name)) |n| n else s.name;
for (structs.items) |added| {
if (eql(added.name, name)) {
std.log.debug("{s} is customized", .{name});
continue :outer;
}
}
if (i < structs.items.len) {
try structs.insert(i, try parseRaylibStruct(allocator, s));
} else {
try structs.append(try parseRaylibStruct(allocator, s));
}
}
for (rlJson.defines.values()) |_| {}
outer: for (rlJson.functions.values(), 0..) |f, i| {
for (functions.items) |added| {
if (eql(added.name, f.name)) {
std.log.debug("{s} is customized", .{f.name});
continue :outer;
}
}
if (i < functions.items.len) {
try functions.insert(i, try parseRaylibFunction(allocator, f));
} else {
try functions.append(try parseRaylibFunction(allocator, f));
}
}
self.enums = try enums.toOwnedSlice();
self.structs = try structs.toOwnedSlice();
self.functions = try functions.toOwnedSlice();
self.defines = try defines.toOwnedSlice();
}
pub fn containsStruct(self: @This(), name: []const u8) bool {
for (self.structs) |s| {
if (eql(s.name, name)) return true;
}
return false;
}
pub fn containsEnum(self: @This(), name: []const u8) bool {
for (self.enums) |e| {
if (eql(e.name, name)) return true;
}
return false;
}
pub fn containsDefine(self: @This(), name: []const u8) bool {
for (self.defines) |d| {
if (eql(d.name, name)) return true;
}
return false;
}
};
pub const Function = struct {
name: []const u8,
params: []FunctionParameter,
returnType: []const u8,
description: ?[]const u8 = null,
custom: bool = false,
};
pub const FunctionParameter = struct {
name: []const u8,
typ: []const u8,
description: ?[]const u8 = null,
};
pub fn parseRaylibFunction(allocator: Allocator, func: RaylibFunction) !Function {
var args = std.ArrayList(FunctionParameter).init(allocator);
if (func.params) |params| {
for (params) |p| {
const t = try toZig(allocator, p.type);
try args.append(.{
.name = p.name,
.typ = t,
.description = p.description,
});
}
}
const returnType = try toZig(allocator, func.returnType);
return Function{
.name = func.name,
.params = try args.toOwnedSlice(),
.returnType = returnType,
.description = func.description,
};
}
pub const Struct = struct {
name: []const u8,
fields: []const StructField,
description: ?[]const u8 = null,
custom: bool = false,
};
pub const StructField = struct {
name: []const u8,
typ: []const u8,
description: ?[]const u8 = null,
};
pub fn parseRaylibStruct(allocator: Allocator, s: RaylibStruct) !Struct {
var fields = std.ArrayList(StructField).init(allocator);
for (s.fields) |field| {
var typ = try toZig(allocator, getTypeWithoutArrayNotation(field.type));
if (getArraySize(field.type)) |size| {
typ = try std.fmt.allocPrint(allocator, "[{d}]{s}", .{ size, typ });
}
try fields.append(.{
.name = field.name,
.typ = typ,
.description = field.description,
});
}
return Struct{
.name = if (alias.get(s.name)) |a| a else s.name,
.fields = try fields.toOwnedSlice(),
.description = s.description,
};
}
pub const Define = struct {
name: []const u8,
typ: []const u8,
value: []const u8,
description: ?[]const u8 = null,
custom: bool = false,
};
pub fn parseRaylibDefine(allocator: Allocator, s: RaylibDefine) ?Define {
var typ: []const u8 = undefined;
var value: []const u8 = undefined;
if (eql("INT", s.type)) {
typ = "i32";
value = std.fmt.allocPrint(allocator, "{s}", .{s.value}) catch return null;
} else if (eql("LONG", s.type)) {
typ = "i64";
value = std.fmt.allocPrint(allocator, "{s}", .{s.value}) catch return null;
} else if (eql("FLOAT", s.type)) {
typ = "f32";
value = std.fmt.allocPrint(allocator, "{s}", .{s.value}) catch return null;
} else if (eql("DOUBLE", s.type)) {
typ = "f64";
value = std.fmt.allocPrint(allocator, "{s}", .{s.value}) catch return null;
} else if (eql("STRING", s.type)) {
typ = "[]const u8";
value = std.fmt.allocPrint(allocator, "\"{s}\"", .{s.value}) catch return null;
} else if (eql("COLOR", s.type)) {
typ = "Color";
std.debug.assert(startsWith(s.value, "CLITERAL(Color){"));
std.debug.assert(endsWith(s.value, "}"));
const componentString = s.value["CLITERAL(Color){".len .. s.value.len - 1];
var spliterator = std.mem.split(u8, componentString, ",");
var r = spliterator.next() orelse return null;
r = std.mem.trim(u8, r, " \t\r\n");
var g = spliterator.next() orelse return null;
g = std.mem.trim(u8, g, " \t\r\n");
var b = spliterator.next() orelse return null;
b = std.mem.trim(u8, b, " \t\r\n");
var a = spliterator.next() orelse return null;
a = std.mem.trim(u8, a, " \t\r\n");
value = std.fmt.allocPrint(allocator, ".{{.r={s}, .g={s}, .b={s}, .a={s}}}", .{ r, g, b, a }) catch return null;
} else {
return null;
}
return Define{
.name = s.name,
.typ = typ,
.value = value,
.description = s.description,
};
}
pub const Enum = struct {
name: []const u8,
values: []const EnumValue,
description: ?[]const u8 = null,
custom: bool = false,
};
pub const EnumValue = struct {
name: []const u8,
value: c_int,
description: ?[]const u8 = null,
};
pub fn parseRaylibEnum(allocator: Allocator, e: RaylibEnum) !Enum {
var values = std.ArrayList(EnumValue).init(allocator);
for (e.values) |value| {
try values.append(.{
.name = value.name,
.value = value.value,
.description = value.description,
});
}
return Enum{
.name = e.name,
.values = try values.toOwnedSlice(),
.description = e.description,
};
}
/// is c const type
pub fn isConst(c: []const u8) bool {
return startsWith(c, "const ");
}
test "isConst" {
try expect(!isConst("char *"));
try expect(!isConst("unsigned char *"));
try expect(isConst("const unsigned char *"));
try expect(isConst("const unsigned int *"));
try expect(isConst("const void *"));
try expect(!isConst("Vector2 *"));
try expect(!isConst("Vector2"));
try expect(!isConst("int"));
}
/// is c pointer type
pub fn isPointer(c: []const u8) bool {
return endsWith(c, "*");
}
test "isPointer" {
try expect(isPointer("char *"));
try expect(isPointer("unsigned char *"));
try expect(isPointer("const unsigned char *"));
try expect(isPointer("const unsigned int *"));
try expect(isPointer("Vector2 *"));
try expect(!isPointer("Vector2"));
try expect(!isPointer("int"));
}
pub fn isPointerToPointer(c: []const u8) bool {
return endsWith(c, "**");
}
test "isPointerToPointer" {
try expect(!isPointerToPointer("char *"));
try expect(!isPointerToPointer("unsigned char *"));
try expect(isPointerToPointer("const unsigned char **"));
try expect(isPointerToPointer("const unsigned int **"));
try expect(isPointerToPointer("Vector2 **"));
try expect(!isPointerToPointer("Vector2*"));
try expect(!isPointerToPointer("int"));
}
pub fn isVoid(c: []const u8) bool {
return eql(stripType(c), "void");
}
test "isVoid" {
try expect(!isVoid("char *"));
try expect(!isVoid("unsigned char *"));
try expect(!isVoid("const unsigned char *"));
try expect(isVoid("const void *"));
try expect(isVoid("void *"));
try expect(isVoid("void"));
try expect(isVoid("void **"));
}
/// strips const and pointer annotations
/// const TName * -> TName
pub fn stripType(c: []const u8) []const u8 {
var name = if (isConst(c)) c["const ".len..] else c;
name = if (isPointer(name)) name[0 .. name.len - 1] else name;
// double pointer?
name = if (isPointer(name)) name[0 .. name.len - 1] else name;
name = std.mem.trim(u8, name, " \t\n");
if (alias.get(name)) |ali| {
name = ali;
}
return name;
}
test "stripType" {
try expectEqualStrings("void", stripType("const void *"));
try expectEqualStrings("unsinged int", stripType("unsinged int *"));
try expectEqualStrings("Vector2", stripType("const Vector2 *"));
}
pub fn getArraySize(typ: []const u8) ?usize {
if (std.mem.indexOf(u8, typ, "[")) |open| {
if (std.mem.indexOf(u8, typ, "]")) |close| {
return std.fmt.parseInt(usize, typ[open + 1 .. close], 10) catch null;
}
}
return null;
}
test "getArraySize" {
const expectEqual = std.testing.expectEqual;
try expectEqual(@as(?usize, 4), getArraySize("float[4]"));
try expectEqual(@as(?usize, 44), getArraySize("int[44]"));
try expectEqual(@as(?usize, 123456), getArraySize("a[123456]"));
try expectEqual(@as(?usize, 1), getArraySize("test[1] "));
try expectEqual(@as(?usize, null), getArraySize("foo[]"));
try expectEqual(@as(?usize, null), getArraySize("bar"));
try expectEqual(@as(?usize, null), getArraySize("foo["));
try expectEqual(@as(?usize, null), getArraySize("bar]"));
try expectEqual(@as(?usize, 42), getArraySize(" lol this is ok[42] "));
}
pub fn getTypeWithoutArrayNotation(typ: []const u8) []const u8 {
if (std.mem.indexOf(u8, typ, "[")) |open| {
return typ[0..open];
}
return typ;
}
fn toZig(allocator: Allocator, c: []const u8) ![]const u8 {
if (fixedMapping.get(c)) |fixed| {
return fixed;
}
const consT = if (isConst(c)) "const " else "";
const pointeR = if (isPointer(c)) "?[*]" else "";
const stripped = stripType(c);
const name = if (raylibToZigType.get(stripped)) |primitive| primitive else stripped;
if (isPointer(c)) {
return try fmt(allocator, "{s}{s}{s}", .{ pointeR, consT, name });
}
return name;
}
test "toZig" {
var arena = std.heap.ArenaAllocator.init(talloc);
defer arena.deinit();
const a = arena.allocator();
try expectEqualStrings("i32", try toZig(a, "int"));
try expectEqualStrings("const i32", try toZig(a, "const int"));
try expectEqualStrings("?[*]Vector2", try toZig(a, "Vector2 *"));
}
const raylibToZigType = std.ComptimeStringMap([]const u8, .{
.{ "void", "void" },
.{ "bool", "bool" },
.{ "char", "u8" },
.{ "unsigned char", "u8" },
.{ "short", "i16" },
.{ "unsigned short", "u16" },
.{ "int", "i32" },
.{ "unsigned int", "u32" },
.{ "long", "i64" },
.{ "unsigned long", "u64" },
.{ "unsigned long long", "u64" },
.{ "float", "f32" },
.{ "double", "f64" },
});
const fixedMapping = std.ComptimeStringMap([]const u8, .{
.{ "void *", "*anyopaque" },
.{ "const void *", "*const anyopaque" },
.{ "const unsigned char *", "[*:0]const u8" },
.{ "const char *", "[*:0]const u8" },
.{ "const char **", "[*]const [*:0]const u8" },
.{ "char **", "[*][*:0]u8" },
.{ "rAudioBuffer *", "*anyopaque" },
.{ "rAudioProcessor *", "*anyopaque" },
.{ "Image *", "*Image" },
});
//--- Raylib parser JSONs -------------------------------------------------------------------------
const alias = std.ComptimeStringMap([]const u8, .{
.{ "Camera", "Camera3D" },
.{ "Texture", "Texture2D" },
.{ "TextureCubemap", "Texture2D" },
.{ "RenderTexture", "RenderTexture2D" },
.{ "GuiStyle", "u32" },
.{ "Quaternion", "Vector4" },
.{ "PhysicsBody", "*PhysicsBodyData" },
});
const cAlias = std.ComptimeStringMap([]const u8, .{
.{ "Camera", "Camera3D" },
.{ "Texture", "Texture2D" },
.{ "TextureCubemap", "Texture2D" },
.{ "RenderTexture", "RenderTexture2D" },
.{ "Quaternion", "Vector4" },
.{ "PhysicsBody", "PhysicsBodyData *" },
});
pub const CombinedRaylib = struct {
structs: std.StringArrayHashMap(RaylibStruct),
enums: std.StringArrayHashMap(RaylibEnum),
defines: std.StringArrayHashMap(RaylibDefine),
functions: std.StringArrayHashMap(RaylibFunction),
pub fn load(allocator: Allocator, jsonFiles: []const []const u8) !@This() {
var structs = std.StringArrayHashMap(RaylibStruct).init(allocator);
var enums = std.StringArrayHashMap(RaylibEnum).init(allocator);
var defines = std.StringArrayHashMap(RaylibDefine).init(allocator);
var functions = std.StringArrayHashMap(RaylibFunction).init(allocator);
for (jsonFiles) |jsonFile| {
std.log.info("parsing {s}", .{jsonFile});
const jsonData = try std.fs.cwd().readFileAlloc(allocator, jsonFile, memoryConstrain);
const bindingJson = try json.parseFromSliceLeaky(RaylibJson, allocator, jsonData, .{
.ignore_unknown_fields = true,
.duplicate_field_behavior = .use_last,
});
for (bindingJson.structs) |*s| {
for (s.fields) |*f| {
f.type = cAlias.get(f.type) orelse f.type;
}
try structs.put(s.name, s.*);
}
for (bindingJson.enums) |e| {
try enums.put(e.name, e);
}
for (bindingJson.defines) |d| {
try defines.put(d.name, d);
}
for (bindingJson.functions) |*f| {
f.returnType = cAlias.get(f.returnType) orelse f.returnType;
if (f.params) |params| {
for (params) |*p| {
p.type = cAlias.get(p.type) orelse p.type;
}
}
try functions.put(f.name, f.*);
}
}
return @This(){
.structs = structs,
.enums = enums,
.defines = defines,
.functions = functions,
};
}
pub fn toIntermediate(self: @This(), allocator: Allocator, customs: Intermediate) !Intermediate {
var functions = std.ArrayList(Function).init(allocator);
var enums = std.ArrayList(Enum).init(allocator);
var structs = std.ArrayList(Struct).init(allocator);
enums: for (self.enums.values()) |e| {
for (customs.enums) |tt| {
if (eql(tt.name, e.name)) break :enums;
}
try enums.append(try parseRaylibEnum(allocator, e));
}
structs: for (self.structs.values()) |s| {
for (customs.structs) |tt| {
if (eql(tt.name, s.name)) break :structs;
}
try structs.append(try parseRaylibStruct(allocator, s, customs));
}
for (self.defines.values()) |_| {}
funcs: for (self.functions.values()) |f| {
for (customs.functions) |ff| {
if (eql(ff.name, f.name)) break :funcs;
}
try functions.append(try parseRaylibFunction(allocator, f, customs.types));
}
return Intermediate{
.functions = try functions.toOwnedSlice(),
.enums = try enums.toOwnedSlice(),
.structs = try structs.toOwnedSlice(),
};
}
fn containsStruct(self: @This(), name: []const u8) bool {
return self.structs.contains(name);
}
fn containsEnum(self: @This(), name: []const u8) bool {
return self.enums.contains(name);
}
fn containsFunction(self: @This(), name: []const u8) bool {
return self.functions.contains(name);
}
};
const RaylibJson = struct {
structs: []RaylibStruct,
enums: []RaylibEnum,
defines: []RaylibDefine,
functions: []RaylibFunction,
};
pub const RaylibStruct = struct {
name: []const u8,
description: []const u8,
fields: []RaylibField,
};
pub const RaylibField = struct {
name: []const u8,
description: []const u8,
type: []const u8,
};
pub const RaylibEnum = struct {
name: []const u8,
description: []const u8,
values: []RaylibEnumValue,
};
pub const RaylibEnumValue = struct {
name: []const u8,
description: []const u8,
value: c_int,
};
pub const RaylibFunction = struct {
name: []const u8,
description: []const u8,
returnType: []const u8,
params: ?[]RaylibFunctionParam = null,
};
pub const RaylibFunctionParam = struct {
name: []const u8,
type: []const u8,
description: ?[]const u8 = null,
};
pub const RaylibDefine = struct {
name: []const u8,
type: []const u8,
value: []const u8,
description: ?[]const u8 = null,
};
//--- Helpers -------------------------------------------------------------------------------------
/// true if C type is primitive or a pointer to anything
/// this means we don't need to wrap it in a pointer
pub fn isPrimitiveOrPointer(c: []const u8) bool {
const primitiveTypes = std.ComptimeStringMap(void, .{
.{ "void", {} },
.{ "bool", {} },
.{ "char", {} },
.{ "unsigned char", {} },
.{ "short", {} },
.{ "unsigned short", {} },
.{ "int", {} },
.{ "unsigned int", {} },
.{ "long", {} },
.{ "unsigned long", {} },
.{ "unsigned long long", {} },
.{ "float", {} },
.{ "double", {} },
});
return primitiveTypes.has(stripType(c)) or endsWith(c, "*");
}
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);
}