421 lines
15 KiB
421 lines
15 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Text; |
|
using ClangSharp.Interop; |
|
|
|
namespace gaemstone.ECS.BindGen; |
|
|
|
public unsafe static class Program |
|
{ |
|
// TODO: Handle being called from other working directories gracefully. |
|
private const string FlecsRepositoryLocation = "../flecs"; |
|
private const string OutputFolderLocation = "../gaemstone.ECS/flecs"; |
|
|
|
private static readonly Dictionary<string, string> TypeAliases = new() { |
|
{ "int8_t" , "sbyte" }, |
|
{ "int16_t", "short" }, |
|
{ "int32_t", "int" }, |
|
{ "int64_t", "long" }, |
|
|
|
{ "uint8_t" , "byte" }, |
|
{ "uint16_t", "ushort" }, |
|
{ "uint32_t", "uint" }, |
|
{ "uint64_t", "ulong" }, |
|
|
|
{ "ecs_flags8_t" , "byte" }, |
|
{ "ecs_flags16_t", "ushort" }, |
|
{ "ecs_flags32_t", "uint" }, |
|
{ "ecs_flags64_t", "ulong" }, |
|
|
|
{ "ecs_size_t", "int" }, |
|
}; |
|
|
|
public static void Main() |
|
{ |
|
using var fileConstants = File.CreateText(Path.Combine(OutputFolderLocation, "flecs+Constants.g.cs")); |
|
using var fileStructs = File.CreateText(Path.Combine(OutputFolderLocation, "flecs+Structs.g.cs")); |
|
using var fileFunctions = File.CreateText(Path.Combine(OutputFolderLocation, "flecs+Functions.g.cs")); |
|
|
|
static string StartProcessAndReturnOutput(string program, string arguments, string workDir = "") |
|
{ |
|
var gitProcess = new Process { StartInfo = new() { |
|
FileName = "git", Arguments = "describe --tags --always", WorkingDirectory = workDir, |
|
RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, |
|
} }; |
|
gitProcess.Start(); |
|
var result = gitProcess.StandardOutput.ReadToEnd().Trim(); |
|
gitProcess.WaitForExit(); |
|
return result; |
|
} |
|
|
|
var bindgenVersion = StartProcessAndReturnOutput("git", "describe --tags --always"); |
|
var flecsVersion = StartProcessAndReturnOutput("git", "describe --tags --always", FlecsRepositoryLocation); |
|
|
|
var autoGeneratedHeader = $$""" |
|
// <auto-generated> |
|
// Generated by gaemstone.ECS.BindGen {{ bindgenVersion }} |
|
// Time: {{ DateTime.UtcNow :u}} |
|
// Flecs version: {{ flecsVersion }} |
|
// </auto-generated> |
|
"""; |
|
|
|
fileConstants.WriteLine($$""" |
|
{{ autoGeneratedHeader }} |
|
|
|
#pragma warning disable CS8981 |
|
public static partial class flecs |
|
{ |
|
"""); |
|
|
|
fileStructs.Write($$""" |
|
{{ autoGeneratedHeader }} |
|
|
|
using System.Runtime.InteropServices; |
|
|
|
#pragma warning disable CS8981 |
|
public static unsafe partial class flecs |
|
{ |
|
"""); |
|
|
|
fileFunctions.WriteLine($$""" |
|
{{ autoGeneratedHeader }} |
|
|
|
using System; |
|
using System.Runtime.InteropServices; |
|
|
|
#pragma warning disable CS8981 |
|
public static unsafe partial class flecs |
|
{ |
|
private const string LibraryName = "flecs"; |
|
|
|
/// <summary> Indicates types are marked as "const" on C's end. </summary> |
|
[AttributeUsage(AttributeTargets.Parameter, AttributeTargets.ReturnValue)] |
|
public class ConstAttribute : Attribute { } |
|
"""); |
|
|
|
using var unit = CXTranslationUnit.CreateFromSourceFile( |
|
CXIndex.Create(), Path.Combine(FlecsRepositoryLocation, "flecs.h"), |
|
default, default); |
|
|
|
unit.Cursor.VisitChildren((cursor, _, _) => { |
|
switch (cursor) { |
|
case { Location.IsFromMainFile: false }: // Not from the file we're trying to parse. |
|
case { Kind: CXCursorKind.CXCursor_MacroInstantiation |
|
or CXCursorKind.CXCursor_InclusionDirective }: // Ignore these altogether. |
|
break; |
|
|
|
case { Kind: CXCursorKind.CXCursor_MacroDefinition }: |
|
WriteMacro(fileConstants, 1, cursor); |
|
break; |
|
|
|
// case { Kind: CXCursorKind.CXCursor_TypedefDecl }: |
|
// WriteTypedef(cursor, output); |
|
// break; |
|
|
|
case { Kind: CXCursorKind.CXCursor_EnumDecl }: |
|
fileStructs.WriteLine(); |
|
WriteEnum(fileStructs, 1, cursor); |
|
break; |
|
|
|
case { Kind: CXCursorKind.CXCursor_StructDecl }: |
|
// Skip forward declarations, unless they're not defined at all. |
|
if (!cursor.IsDefinition && !cursor.Definition.IsNull) break; |
|
fileStructs.WriteLine(); |
|
WriteStruct(fileStructs, 1, cursor); |
|
break; |
|
|
|
case { Kind: CXCursorKind.CXCursor_FunctionDecl }: |
|
fileFunctions.WriteLine(); |
|
WriteFunction(fileFunctions, 1, cursor); |
|
break; |
|
|
|
default: |
|
Console.WriteLine($"{cursor.Kind} {cursor}"); |
|
Console.WriteLine(GetSource(unit, cursor.Extent)); |
|
break; |
|
} |
|
return CXChildVisitResult.CXChildVisit_Continue; |
|
}, default); |
|
|
|
fileConstants.WriteLine("}"); |
|
fileStructs .WriteLine("}"); |
|
fileFunctions.WriteLine("}"); |
|
} |
|
|
|
|
|
private static void WriteMacro(StreamWriter writer, int indent, CXCursor cursor) |
|
{ |
|
if (cursor.IsMacroFunctionLike) return; |
|
var unit = cursor.TranslationUnit; |
|
var tokens = unit.Tokenize(cursor.Extent).ToArray(); |
|
if (tokens.Length < 2) return; // No value. |
|
|
|
var type = "uint"; |
|
var name = cursor.Spelling.ToString(); |
|
if (name is "NULL" or "ECS_VECTOR_T_SIZE" |
|
or "EcsLastInternalComponentId" |
|
or "ECS_FUNC_NAME_BACK") return; |
|
|
|
var value = GetSource(unit, Range(unit, tokens[1], tokens[^1])); |
|
if (value is not [ '(', .., ')']) return; |
|
if (value.Contains('\\')) value = value.Replace("\\\n", "").Replace(" ", ""); |
|
if (value.Contains("ull")) { value = value.Replace("ull", "ul"); type = "ulong"; } |
|
if (name == "ECS_MAX_COMPONENT_ID") value = value.Replace("(uint32_t)", "(uint)"); |
|
|
|
WriteComment(writer, indent, cursor); |
|
WriteIndent(writer, indent).WriteLine($"public const {type} {name} = {value};"); |
|
} |
|
|
|
private static void WriteEnum(StreamWriter writer, int indent, CXCursor cursor) |
|
{ |
|
WriteComment(writer, indent, cursor); |
|
|
|
WriteIndent(writer, indent).WriteLine($"public {cursor.Type}"); |
|
WriteIndent(writer, indent).WriteLine("{"); |
|
|
|
cursor.VisitChildren((value, _, _) => { |
|
WriteComment(writer, indent + 1, value); |
|
WriteIndent(writer, indent + 1).WriteLine($"{value},"); |
|
return CXChildVisitResult.CXChildVisit_Continue; |
|
}, default); |
|
|
|
WriteIndent(writer, indent).WriteLine("}"); |
|
} |
|
|
|
private static void WriteStruct(StreamWriter writer, int indent, CXCursor cursor, |
|
string? name = null, bool noComment = false) |
|
{ |
|
if (!noComment) WriteComment(writer, indent, cursor); |
|
|
|
if (cursor.Kind == CXCursorKind.CXCursor_UnionDecl) |
|
WriteIndent(writer, indent).WriteLine("[StructLayout(LayoutKind.Explicit)]"); |
|
|
|
name ??= cursor.Type.ToString(); |
|
if (name.StartsWith("struct ")) name = name[("struct ".Length)..]; |
|
WriteIndent(writer, indent).Write($"public struct {name}"); |
|
|
|
var hasFields = false; // For creating a shorthand struct definition that doesn't take up 3 lines. |
|
cursor.VisitChildren((field, _, _) => { |
|
// Nested struct and union declarations will be handled when field is written. |
|
if (field.Kind is CXCursorKind.CXCursor_StructDecl |
|
or CXCursorKind.CXCursor_UnionDecl) |
|
return CXChildVisitResult.CXChildVisit_Continue; |
|
|
|
if (field.Kind is not CXCursorKind.CXCursor_FieldDecl) |
|
throw new NotSupportedException(); |
|
|
|
if (!hasFields) { |
|
writer.WriteLine(); |
|
WriteIndent(writer, indent).WriteLine("{"); |
|
hasFields = true; |
|
} |
|
|
|
if (cursor.Kind == CXCursorKind.CXCursor_UnionDecl) |
|
WriteIndent(writer, indent).WriteLine("[FieldOffset(0)]"); |
|
|
|
WriteComment(writer, indent + 1, field); |
|
WriteIndent(writer, indent + 1); |
|
|
|
var name = field.DisplayName.ToString(); |
|
if (IsReservedKeyword(name)) name = $"@{name}"; |
|
|
|
if (field.Type.Declaration.IsAnonymous) { |
|
writer.WriteLine($"public _{field.DisplayName} {name};"); |
|
WriteStruct(writer, indent + 1, field.Type.Declaration, |
|
name: $"_{field.DisplayName}", noComment: true); |
|
} else if (field.Type.kind == CXTypeKind.CXType_ConstantArray) { |
|
writer.WriteLine($"public fixed {field.Type.ArrayElementType} {name}[{field.Type.ArraySize}];"); |
|
} else { |
|
var type = GetTypeString(field.Type, out var isConst); |
|
if (isConst) writer.Write($"[Const] "); |
|
writer.WriteLine($"public {type} {name};"); |
|
} |
|
|
|
return CXChildVisitResult.CXChildVisit_Continue; |
|
}, default); |
|
if (!hasFields) writer.WriteLine(" { }"); |
|
else WriteIndent(writer, indent).WriteLine("}"); |
|
} |
|
|
|
private static void WriteFunction(StreamWriter writer, int indent, CXCursor cursor) |
|
{ |
|
WriteComment(writer, indent, cursor); |
|
|
|
var returnType = GetTypeString(cursor.ReturnType, out var returnIsConst); |
|
if (returnIsConst) WriteIndent(writer, indent).WriteLine("[return: Const]"); |
|
WriteIndent(writer, indent).WriteLine("[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]"); |
|
WriteIndent(writer, indent).Write($"public static extern {returnType} {cursor.Spelling}("); |
|
|
|
for (var i = 0; i < cursor.NumArguments; i++) { |
|
var arg = cursor.GetArgument((uint)i); |
|
if (i > 0) writer.Write(", "); |
|
|
|
var argType = GetTypeString(arg.Type, out var argIsConst); |
|
if (argIsConst) writer.Write("[Const] "); |
|
var name = arg.DisplayName.ToString(); |
|
if (IsReservedKeyword(name)) name = $"@{name}"; |
|
writer.Write($"{argType} {name}"); |
|
} |
|
|
|
writer.WriteLine(");"); |
|
} |
|
|
|
private static void WriteComment(StreamWriter writer, int indent, CXCursor cursor) |
|
{ |
|
var comment = cursor.ParsedComment; |
|
if (comment.Kind == CXCommentKind.CXComment_Null) return; |
|
if (comment.Kind != CXCommentKind.CXComment_FullComment) throw new NotSupportedException(); |
|
|
|
var children = Children(comment).ToArray(); |
|
var paragraphs = children |
|
.TakeWhile(c => c.Kind == CXCommentKind.CXComment_Paragraph) |
|
.Select(GetCommentText).Where(p => p != null).ToArray(); |
|
var @params = children |
|
.Where (c => c.Kind == CXCommentKind.CXComment_ParamCommand) |
|
.Select(c => (c.ParamCommandComment_ParamName, GetCommentText(Children(c).Single()))).ToArray(); |
|
var @return = children |
|
.Where (c => c.Kind == CXCommentKind.CXComment_BlockCommand) |
|
.Where (c => c.BlockCommandComment_CommandName.ToString() == "return") |
|
.Select(c => GetCommentText(Children(c).Single())).FirstOrDefault(); |
|
|
|
if (paragraphs.Length > 1) { |
|
WriteIndent(writer, indent).WriteLine($"/// <summary>"); |
|
foreach (var paragraph in paragraphs) |
|
WriteIndent(writer, indent).WriteLine($"/// <p> {paragraph} </p>"); |
|
WriteIndent(writer, indent).WriteLine($"/// </summary>"); |
|
} else if (paragraphs.Length == 1) |
|
WriteIndent(writer, indent).WriteLine($"/// <summary> {paragraphs[0]} </summary>"); |
|
|
|
foreach (var (name, text) in @params) |
|
WriteIndent(writer, indent).WriteLine($$"""/// <param name="{{ name }}"> {{ text }} </param>"""); |
|
|
|
if (@return != null) |
|
WriteIndent(writer, indent).WriteLine($$"""/// <returns> {{ @return }} </returns>"""); |
|
|
|
foreach (var child in Children(comment)) |
|
switch (child.Kind) { |
|
case CXCommentKind.CXComment_Paragraph: |
|
case CXCommentKind.CXComment_ParamCommand: |
|
case CXCommentKind.CXComment_BlockCommand when child.BlockCommandComment_CommandName.ToString() == "return": |
|
break; |
|
default: |
|
WriteIndent(writer, indent); |
|
writer.WriteLine($"//// {child.Kind}"); |
|
break; |
|
} |
|
|
|
static IEnumerable<CXComment> Children(CXComment parent) |
|
=> Enumerable.Range(0, (int)parent.NumChildren).Select(i => parent.GetChild((uint)i)); |
|
|
|
static string? GetCommentText(CXComment paragraph) |
|
{ |
|
if (paragraph.Kind != CXCommentKind.CXComment_Paragraph) throw new NotSupportedException(); |
|
var sb = new StringBuilder(); |
|
foreach (var part in Children(paragraph)) { |
|
if (part.Kind != CXCommentKind.CXComment_Text) throw new NotSupportedException(); |
|
var str = part.TextComment_Text.ToString().Trim(); |
|
if (str == "") continue; |
|
if (sb.Length > 0) sb.Append(' '); |
|
sb.Append(str); |
|
} |
|
if (sb.Length == 0) return null; |
|
sb.Replace("<", "<").Replace(">", ">"); |
|
if (sb[^1] != '.') sb.Append('.'); |
|
return sb.ToString(); |
|
} |
|
} |
|
|
|
private static StreamWriter WriteIndent(StreamWriter writer, int indent) |
|
{ |
|
for (var i = 0; i < indent; i++) |
|
writer.Write('\t'); |
|
return writer; |
|
} |
|
|
|
|
|
private static CXSourceRange Range(CXTranslationUnit unit, CXToken start, CXToken end) |
|
=> CXSourceRange.Create(start.GetExtent(unit).Start, end.GetExtent(unit).End); |
|
|
|
private static string GetTypeString(CXType type, out bool isConst) |
|
{ |
|
isConst = false; |
|
switch (type) { |
|
case { kind: CXTypeKind.CXType_Pointer, |
|
PointeeType: { kind: CXTypeKind.CXType_FunctionProto } funcType }: |
|
var resultType = GetTypeString(funcType.ResultType, out _); |
|
var argTypes = Enumerable.Range(0, funcType.NumArgTypes) |
|
.Select(i => funcType.GetArgType((uint)i)) |
|
.Select(a => GetTypeString(a, out _)) |
|
.ToArray(); |
|
return $"delegate* unmanaged<{string.Join(", ", argTypes)}, {resultType}>"; |
|
|
|
case { kind: CXTypeKind.CXType_Pointer, |
|
PointeeType: { kind: CXTypeKind.CXType_Elaborated, |
|
Declaration.IsDefined: false} }: |
|
return "void*"; |
|
|
|
case { kind: CXTypeKind.CXType_Pointer }: |
|
return GetTypeString(type.PointeeType, out isConst) + "*"; |
|
|
|
case { kind: CXTypeKind.CXType_VariableArray |
|
or CXTypeKind.CXType_IncompleteArray }: |
|
return GetTypeString(type.ArrayElementType, out isConst) + "[]"; |
|
|
|
case { kind: CXTypeKind.CXType_Record }: |
|
throw new NotSupportedException(); |
|
|
|
case { kind: CXTypeKind.CXType_Elaborated }: |
|
var name = type.Declaration.ToString(); |
|
if (name == "") throw new Exception(); |
|
return name; |
|
|
|
case { kind: CXTypeKind.CXType_FunctionProto }: |
|
throw new NotSupportedException(); |
|
|
|
default: |
|
name = type.ToString(); |
|
if (name == "") throw new Exception(); |
|
if (type.IsConstQualified) { |
|
if (!name.StartsWith("const ")) throw new Exception(); |
|
name = name[("const ".Length)..]; |
|
isConst = true; |
|
} |
|
if (name.Contains(' ')) throw new Exception(); |
|
if (TypeAliases.TryGetValue(name, out var alias)) name = alias; |
|
return name; |
|
} |
|
} |
|
|
|
private static string GetSource(CXTranslationUnit unit, CXSourceRange range) |
|
{ |
|
range.Start.GetFileLocation(out var startFile, out _, out _, out var start); |
|
range.End .GetFileLocation(out var endFile , out _, out _, out var end); |
|
if (startFile != endFile) return string.Empty; |
|
return Encoding.UTF8.GetString( |
|
unit.GetFileContents(startFile, out _) |
|
.Slice((int)start, (int)(end - start))); |
|
} |
|
|
|
// See https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ |
|
// Reserved keywords are unlikely to change in future C# versions, as they'd break |
|
// existing programs. New ones are instead added as contextual keywords. |
|
private static bool IsReservedKeyword(string str) |
|
=> str is "abstract" or "as" or "base" or "bool" or "break" or "byte" |
|
or "case" or "catch" or "char" or "checked" or "class" |
|
or "const" or "continue" or "decimal" or "default" |
|
or "delegate" or "do" or "double" or "else" or "enum" |
|
or "event" or "explicit" or "extern" or "false" or "finally" |
|
or "fixed" or "float" or "for" or "foreach" or "goto" or "if" |
|
or "implicit" or "in" or "int" or "interface" or "internal" |
|
or "is" or "lock" or "long" or "namespace" or "new" or "null" |
|
or "object" or "operator" or "out" or "override" or "params" |
|
or "private" or "protected" or "public" or "readonly" or "ref" |
|
or "return" or "sbyte" or "sealed" or "short" or"sizeof" |
|
or "stackalloc" or "static" or "string" or "struct" or "switch" |
|
or "this" or "throw" or "true" or "try" or "typeof" or "uint" |
|
or "ulong" or "unchecked" or "unsafe" or "ushort" or "using" |
|
or "virtual" or "void" or "volatile" or "while"; |
|
}
|
|
|