raylib-zig/lib/generate_functions.py

437 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
import re
"""
Automatic utility for generating raylib function headers.
"""
C_TO_ZIG = {
"bool": "bool",
"char": "u8",
"double": "f64",
"float": "f32",
"int": "c_int",
"long": "c_long",
"unsigned char": "u8",
"unsigned int": "c_uint",
}
ZIGGIFY = {
"c_int": "i32",
"c_long": "i64",
"c_uint": "u32"
}
IGNORE_TYPES = [
"[*c]Color",
"[*c]GlyphInfo",
"[*c]c_int",
"[*c]c_uint",
"[*c][*c]u8",
"[*c][*c]const u8",
"[*c]Material",
"[*c]ModelAnimation",
"[*c]f32",
]
MAKE_CONST = [
"points"
]
# Some C types have a different sizes on different systems and Zig
# knows that so we tell it to get the system specific size for us.
def c_to_zig_type(c: str) -> str:
const = "const " if "const " in c else ""
c = c.replace("const ", "")
z = C_TO_ZIG.get(c)
if z is not None:
return const + z
return const + c
def ziggify_type(name: str, t: str, func_name) -> str:
NO_STRINGS = ["data", "fileData", "compData"]
single = [
"value", "ptr", "bytesRead", "compDataSize", "dataSize", "outputSize",
"camera", "collisionPoint", "frames", "image", "colorCount", "dst",
"texture", "srcPtr", "dstPtr", "count", "codepointSize", "utf8Size",
"position", "mesh", "materialCount", "material", "model", "animCount",
"wave", "v1", "v2", "outAxis", "outAngle", "fileSize",
"AutomationEventList", "list", "batch", "glInternalFormat", "glFormat",
"glType", "mipmaps", "active", "scroll", "view", "checked", "mouseCell",
"scrollIndex", "focus", "secretViewActive", "color", "alpha", "colorHsv"
]
multi = [
"data", "compData", "points", "fileData", "colors", "pixels",
"fontChars", "chars", "recs", "codepoints", "textList", "transforms",
"animations", "samples", "LoadImageColors", "LoadImagePalette",
"LoadFontData", "LoadCodepoints", "TextSplit", "LoadMaterials",
"LoadModelAnimations", "LoadWaveSamples", "images",
"LoadRandomSequence", "sequence", "kernel", "GlyphInfo", "glyphs", "glyphRecs",
"matf", "rlGetShaderLocsDefault", "locs", "GuiGetIcons", "GuiLoadIcons"
]
string = False
if name == "text" and t == "[*c][*c]const u8":
return "[][*:0]const u8"
if t.startswith("[*c]") and name not in single and name not in multi:
if (t == "[*c]const u8" or t == "[*c]u8") and name not in NO_STRINGS: # Strings are multis.
string = True
else:
raise ValueError(f"{t} {name} not classified")
pre = ""
while t.startswith("[*c]"):
t = t[4:]
if string and not t.startswith("[*c]"):
pre += "[*:0]"
elif name in single:
pre += "*"
else:
pre += "[]"
if t in ZIGGIFY:
t = ZIGGIFY[t]
return pre + t
def add_namespace_to_type(t: str) -> str:
pre = ""
while t.startswith("[*c]"):
t = t[4:]
pre += "[*c]"
if t.startswith("const "):
t = t[6:]
pre += "const "
if t.startswith("Gui"):
t = "rgui." + t
elif t[0].isupper():
t = "rl." + t
elif t in ["float3", "float16"]:
t = "rlm." + t
elif t.startswith("rl"):
t = "rlgl." + t
return pre + t
def make_return_cast(source_type: str, dest_type: str, inner: str) -> str:
if source_type == dest_type:
return inner
if source_type in ["[*c]const u8", "[*c]u8"]:
return f"std.mem.span({inner})"
if source_type in ZIGGIFY:
return f"@as({dest_type}, {inner})"
# These all have to be done manually because their sizes depend on the
# function arguments.
if source_type in IGNORE_TYPES:
return None
else:
raise ValueError(f"Don't know what to do {source_type} {dest_type} {inner}")
def fix_pointer(name: str, t: str):
pre = ""
while name.startswith("*"):
name = name[1:]
pre += "[*c]"
if name in MAKE_CONST:
pre += "const "
t = pre + t
if t == "[*c]const void":
t = "*const anyopaque"
elif t == "[*c]void":
t = "*anyopaque"
elif len(pre) == 0:
t = t.replace("const ", "")
return name, t
_fix_enums_data = [
# arg_name, new_type, func_name_regex
("key", "KeyboardKey", r".*"),
("mode", "CameraMode", r"UpdateCamera"),
("mode", "BlendMode", r"BeginBlendMode"),
("gesture", "Gesture", r".*"),
("logLevel", "TraceLogLevel", r".*"),
("ty", "FontType", r".*"),
("uniformType", "ShaderUniformDataType", r".*"),
("cursor", "MouseCursor", r".*"),
("format", "PixelFormat", r".*"),
("newFormat", "PixelFormat", r".*"),
("layout", "CubemapLayout", r".*"),
("mapType", "MaterialMapIndex", r".*"),
("filter", "TextureFilter", r"SetTextureFilter"),
("wrap", "TextureWrap", r"SetTextureWrap"),
("flags", "ConfigFlags", r"SetWindowState|ClearWindowState|SetConfigFlags"),
("flag", "ConfigFlags", r"IsWindowState"),
("flags", "Gesture", r"SetGesturesEnabled"),
("button", "GamepadButton", r".*GamepadButton.*"),
("button", "MouseButton", r".*MouseButton.*"),
("control", "GuiState", r"Gui.etStyle"),
("property", "GuiControlProperty", r"Gui.etStyle"),
]
def fix_enums(arg_name, arg_type, func_name):
if func_name.startswith("rl"):
return arg_type
# Hacking specific enums in here.
# Raylib doesn't use the enums but rather the resulting ints.
if arg_type == "int" or arg_type == "unsigned int":
for target_arg_name,new_type,func_name_regex in _fix_enums_data:
if arg_name==target_arg_name and re.fullmatch(func_name_regex,func_name):
return new_type
return arg_type
def convert_name_case(name):
return name[:1].lower() + name[1:] if name else ''
def parse_header(header_name: str, output_file: str, ext_file: str, prefix: str, prelude_file: str, ext_prelude_file: str, skip_after: str = "#/never\\#"):
header = open(header_name, mode="r")
ext_heads = []
zig_funcs = []
zig_types = set()
leftover = ""
for line in header.readlines():
if line == skip_after:
break
if line.startswith("typedef struct"):
zig_types.add(line.split(' ')[2])
elif line.startswith("typedef enum"):
# Don't trip the general typedef case.
pass
elif line.startswith("typedef "):
zig_types.add(line.split(' ')[2].replace(';', '').strip())
if not line.startswith(prefix):
continue
split_line = line.split(";", 1)
line = split_line[0]
if len(split_line) > 1:
desc = split_line[1].lstrip()
inline_comment = ("/" + desc) if len(desc) > 0 else ""
else:
inline_comment = ""
if leftover:
line = leftover + line
leftover = ""
line = line.replace("* ", " *")
line = line.replace(",", ", ")
line = line.replace(" ", " ")
# Each (.*) is some variable value.
result = re.search(
prefix + "(.*) (.*)start_arg(.*)end_arg(.*)",
line.replace("(", "start_arg").replace(")", "end_arg"),
)
if result is None:
leftover += line
continue
# Get whats in the (.*)'s.
return_type = result.group(1)
func_name = result.group(2)
arguments = result.group(3)
if func_name == "SetTraceLogCallback":
continue
return_type = c_to_zig_type(return_type)
func_name, return_type = fix_pointer(func_name, return_type)
if func_name == "GetKeyPressed":
return_type = "KeyboardKey"
elif func_name == "GetGamepadButtonPressed":
return_type = "GamepadButton"
elif func_name == "GetGestureDetected":
return_type = "Gesture"
zig_c_arguments = []
zig_arguments = []
zig_call_args = []
if not arguments:
arguments = "void"
for arg in arguments.split(", "):
if arg == "void":
break
if arg == "...":
zig_c_arguments.append("...")
continue
# Everything but the last element (for stuff like "const Vector3").
arg_type = " ".join(arg.split(" ")[0:-1])
arg_name = arg.split(" ")[-1] # Last element should be the name.
if arg_name == "type":
arg_name = "ty"
arg_type = fix_enums(arg_name, arg_type, func_name)
arg_type = c_to_zig_type(arg_type)
arg_name, arg_type = fix_pointer(arg_name, arg_type)
single_opt = [
("rlDrawVertexArrayElements", "buffer"),
("rlDrawVertexArrayElementsInstanced", "buffer"),
("rlEnableStatePointer", "buffer"),
("rlSetRenderBatchActive", "batch"),
("rlLoadTexture", "data"),
("rlLoadTextureCubemap", "data"),
("rlLoadShaderBuffer", "data"),
("rlLoadShaderCode", "vsCode"),
("rlLoadShaderCode", "fsCode"),
]
zig_type = ziggify_type(arg_name, arg_type, func_name)
if zig_type.startswith("*") and (func_name, arg_name) in single_opt:
if not arg_type.startswith("[*c]"):
arg_type = "?" + arg_type
zig_type = "?" + zig_type
zig_types.add(arg_type)
zig_c_arguments.append(arg_name + ": " + add_namespace_to_type(arg_type)) # Put everything together.
zig_arguments.append(arg_name + ": " + zig_type)
if arg_type == zig_type:
zig_call_args.append(arg_name)
else:
if arg_type.startswith("[*c]"):
zig_call_args.append(f"@as({arg_type}, @ptrCast({arg_name}))")
else:
zig_call_args.append(f"@as({arg_type}, {arg_name})")
zig_c_arguments = ", ".join(zig_c_arguments)
ext_ret = add_namespace_to_type(return_type)
ext_heads.append(f"pub extern \"c\" fn {func_name}({zig_c_arguments}) {ext_ret};")
zig_name = convert_name_case(func_name)
# TODO: Ziggify return type
zig_arguments = ", ".join(zig_arguments)
zig_call_args = ", ".join(zig_call_args)
manual = [
"TextFormat",
"LoadShader",
"ExportDataAsCode",
"LoadFileData",
"SaveFileData",
"ExportDataAsCode",
"LoadImageFromMemory",
"DrawMeshInstanced",
"UnloadModelAnimations",
"CompressData",
"DecompressData",
"EncodeDataBase64",
"DecodeDataBase64",
"SetWindowIcons",
"CheckCollisionPointPoly",
"LoadFontEx",
"GenImageFontAtlas",
"UnloadFontData",
"DrawTextCodepoints",
"LoadUTF8",
"TextJoin",
"DrawLineStrip",
"DrawTriangleFan",
"DrawTriangleStrip",
"DrawTriangleStrip3D",
"GuiTabBar",
"GuiListViewEx",
"GuiPanel",
"GuiScrollPanel",
"DrawSplineLinear",
"DrawSplineBasis",
"DrawSplineCatmullRom",
"DrawSplineBezierQuadratic",
"DrawSplineBezierCubic",
"ImageKernelConvolution"
]
if func_name in manual or "FromMemory" in func_name:
continue
zig_return = ziggify_type(func_name, return_type, func_name)
return_cast = make_return_cast(return_type, zig_return, f"cdef.{func_name}({zig_call_args})")
if return_cast:
zig_funcs.append(
inline_comment +
f"pub fn {zig_name}({zig_arguments}) {zig_return}" +
" {\n " +
("return " if zig_return != "void" else "") +
return_cast + ";"
"\n}"
)
prelude = open(prelude_file, mode="r").read()
ext_prelude = open(ext_prelude_file, mode="r").read()
ext_header = open(ext_file, mode="w")
print(ext_prelude, file=ext_header)
print("\n".join(ext_heads), file=ext_header)
zig_header = open(output_file, mode="w")
print(prelude, file=zig_header)
print("\n\n".join(zig_funcs), file=zig_header)
if __name__ == "__main__":
parse_header(
"raylib.h",
"raylib.zig",
"raylib-ext.zig",
"RLAPI ",
"preludes/raylib-prelude.zig",
"preludes/raylib-ext-prelude.zig"
)
parse_header(
"raymath.h",
"raymath.zig",
"raymath-ext.zig",
"RMAPI ",
"preludes/raymath-prelude.zig",
"preludes/raymath-ext-prelude.zig"
)
parse_header(
"rlgl.h",
"rlgl.zig",
"rlgl-ext.zig",
"RLAPI ",
"preludes/rlgl-prelude.zig",
"preludes/rlgl-ext-prelude.zig",
"#if defined(RLGL_IMPLEMENTATION)\n"
)
parse_header(
"raygui.h",
"raygui.zig",
"raygui-ext.zig",
"RAYGUIAPI ",
"preludes/raygui-prelude.zig",
"preludes/raygui-ext-prelude.zig",
"#if defined(RAYGUI_IMPLEMENTATION)\n"
)