From a40d2921c91fa059875143b39d562c7f1fd45fdd Mon Sep 17 00:00:00 2001 From: Adam Foflonker <87928802+DnA-IntRicate@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:10:49 +0200 Subject: [PATCH 1/6] Nav add Tooling section --- mkdocs.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 82ba8dd..d0e62a5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,19 +65,21 @@ markdown_extensions: nav: - Home: index.md + - Philosophy: philosophy.md - Project: - - Overview: project/index.md - - Build Configurations: project/build-configurations.md - - IDE Requirements: project/ide-requirements.md - - Setup Guide: project/setup.md - - System Requirements: project/system-requirements.md + - Overview: project/index.md + - Build Configurations: project/build-configurations.md + - IDE Requirements: project/ide-requirements.md + - Setup Guide: project/setup.md + - System Requirements: project/system-requirements.md - Style: - - Overview: style/index.md - - C++ Style Guide: style/cpp-style.md - - C# Style Guide: style/csharp-style.md - - Commenting Conventions: style/comments.md - - Naming Conventions: style/naming-conventions.md - - Philosophy: philosophy.md + - Overview: style/index.md + - C++ Style Guide: style/cpp-style.md + - C# Style Guide: style/csharp-style.md + - Commenting Conventions: style/comments.md + - Naming Conventions: style/naming-conventions.md + - Tooling: + - Overview: tooling/index.md copyright: | © 2026-present Intricate Dev Team From d738454253428c6a402c90560ff1910ce96b7b17 Mon Sep 17 00:00:00 2001 From: Adam Foflonker <87928802+DnA-IntRicate@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:11:02 +0200 Subject: [PATCH 2/6] Write tooling/index.md --- docs/tooling/index.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/tooling/index.md diff --git a/docs/tooling/index.md b/docs/tooling/index.md new file mode 100644 index 0000000..cee1c8d --- /dev/null +++ b/docs/tooling/index.md @@ -0,0 +1,15 @@ +# Tooling + +This section outlines all the various tools written for and used in Intricate. + +--- + +## Code Generators + +Intricate currently employs `3` code generators, namely: + +- [**BindingsGenerator**](bindings-generator.md): Generates C++ to C# interop glue code (or bindings) during build-time +- **ImGuiCodeGenerator**: Generates C# binding wrappers around **cimgui** and **cimguizmo** +- **ResourceGenerator**: Expresses assets/resources as C++ and C# arrays which get compiled into the project + +--- From e43a6fbe7ddbd535384f265e90fc0ac10719f550 Mon Sep 17 00:00:00 2001 From: Adam Foflonker <87928802+DnA-IntRicate@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:01:27 +0200 Subject: [PATCH 3/6] Begin writing tooling/bindings-generator.md --- docs/tooling/bindings-generator.md | 144 +++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 145 insertions(+) create mode 100644 docs/tooling/bindings-generator.md diff --git a/docs/tooling/bindings-generator.md b/docs/tooling/bindings-generator.md new file mode 100644 index 0000000..b87b379 --- /dev/null +++ b/docs/tooling/bindings-generator.md @@ -0,0 +1,144 @@ +# Bindings Generator + +This section discusses the purpose of the **Bindings Generator**, how it works, and how to use it. + +--- + +## Foreword On C# Interop + +For the interop systems to work correctly, there has to be a way for `C#` code to be able to call into `C++` code. The way we do this in Intricate, is by leveraging the [LibraryImport](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation) system in C#, which is built off of the older [DLLImport](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute?view=net-10.0) system. + +For `C#` to be able to call `C++` code, a **LibraryImport** stub method must be present in `C#`, and this stub must map to a valid **exported unmangled** `C++` function/symbol in a **shared** or **static** library (`.dll`/`.lib`). This combination of `C#` binding stub and `C++` binding function is referred to as a **"binding pair"**. + +!!! Example "Example binding pair" + The `C#` binding stub: + + ``` C# + [LibraryImport(LibIntricateEngine, EntryPoint = "RenderCommand_GetAPI")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial GraphicsAPI RenderCommand_GetAPI(); + ``` + + The matching `C++` binding function: + + ``` C++ + _IE_BINDING_QUALIFIER GraphicsAPI RenderCommand_GetAPI() + { + return RenderCommand::GetAPI(); + } + ``` + + > **Note**: `#!C++ _IE_BINDING_QUALIFIER` is a macro which expands to: `#!C++ extern "C" __declspec(dllexport)`. + +With this binding pair present and correctly written; all `C#` code that calls `RenderCommand_GetAPI()` now ultimately calls the `C++` `RenderCommand::GetAPI()` function. + +### Why the underscore naming convention? + +In `C++`, declaring a function as `#!C++ extern "C"` disables all **name-mangling** that would otherwise be performed on this function by the compiler and linker. + +!!! Example "Mangled vs unmangled symbols" + The mangled symbol declared **without** `#!C++ extern "C"`: + `?RenderCommand_GetAPI@Bindings@IntricateEngine@@YA?AW4GraphicsAPI@@XZ` + + The unmangled symbol declared **as** `#!C++ extern "C"`: + `RenderCommand_GetAPI` + +As we can see from the above example, the **unmangled** symbol is much easier to work with, especially since this is the name that must be set for the `EntryPoint` field in `C#`'s `LibraryImportAttribute`: + +``` C# +[LibraryImport(LibIntricateEngine, EntryPoint = "RenderCommand_GetAPI")] +``` + +Using **unmangled** symbols do however present us with a problem: all exported symbols must now be **unique**. This means no two functions can have the same name - including overloaded functions. This is the reason why we write **binding function names** in the format: `TypeName_FunctionName` - this ensures the uniqueness of the symbol. + +### But how do we handle overloads? + +Since all **exported symbols** must be unique, including overloads; we add **underscore suffixes** of the **type names** of the overloaded **parameters** to the binding name. + +!!! Example + Suppose we have `2` overloaded functions in the `SceneCamera` class: + + ``` C++ + void SetViewportSize(uint32 width, uint32 height); + void SetViewportSize(Vector2Int bounds); + ``` + + As binding function declarations (in `C++`), these would be written as: + + ``` C++ + // NOTE: For primitives, the C# type name is preferred, hence 'uint' instead of 'uint32' + _IE_BINDING_QUALIFIER void SceneCamera_SetViewportSize_uint_uint(uint32 width, uint32 height); + _IE_BINDING_QUALIFIER void SceneCamera_SetViewportSize_Vector2Int(Vector2Int bounds); + ``` + +--- + +## ABI Requirements & Calling Conventions + +!!! Note + For the rest of this section, `C++` code will be referred to as **Native code**, and `C#` code will be referred to as **Managed code**. + +For two languages to interoperate, they must agree on the same **Application Binary Interface (ABI)**. The ABI defines the low-level **contract** that both sides must follow, including the **calling convention** used for function calls. + +This agreement covers, among other things: + +- Data layout and alignment in memory +- Which registers are used for parameters and return values +- Stack usage and stack-frame layout + +And as such, all interop binding functions we write must also adhere to ABI requirements - otherwise everything turns into **undefined behaviour**. + +!!! note + In Intricate, on the `x86_64` platform, the **Microsoft x64 ABI** is used. + +### Plain Old Data types (PODs) + +A type is considered a **POD** if: + +- It is a primitive type (`int`, `float`, etc...) +- It is a struct that contains only other PODs +- It is trivially-copyable +- It defines no constructors (in `C++`) + +!!! tip + Even if a `C++` struct contains only POD types and is **trivially-copyable**, the compiler will stop treating it as a POD the moment it sees any explicitly defined constructors. In other words, PODs may **not** adhere to any form of **RAII**. + +### Why PODs matter + +PODs are very important from an ABI perspective as they don't require any explicit **marshalling**. This means that PODs may be directly passed as **function parameters** and **function return-values** in binding functions. + +!!! note + Non-PODs may be passed as **function parameters** so long as the native and matching managed types have the **exact same memory layout**. + +In the **IntricateEngine** codebase, types like `Vector2` may be freely passed around in **binding function parameters** as it is a **trivially copyable** type since it only contains `float` fields - provided that the matching `C#` `Vector2` type is of the same nature with the same **sequential memory layout**. + +!!! danger + `Vector2` **cannot** be directly passed as a **function return-value**. Since `Vector2` contains explicitly-defined **constructors**, the compiler doesn't treat it as a POD - it is treated as a **non-POD type**. And when non-POD types are returned: according to the ABI, since they can't fit inside the return register, a **pointer-to** the return data is passed in the return register rather than the return data itself. This then leads `C#` to interpret the **pointer** to the data as the **data itself** which is very dangerous and highly incorrect. + +### How to return non-PODs + +Non-PODs must be returned as **out pointers**. + +!!! example + Suppose we're writing a **binding pair** named `GetVector`. The `C#` binding would be written as: + + ``` C# + [LibraryImport(LibIntricateEngine, EntryPoint = "GetVector")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial void GetVector(out Vector2 vector); + ``` + + And the `C++` binding would be written as: + + ``` C++ + _IE_BINDING_QUALIFIER void GetVector(Vector2* outVector) + { + // This dereferences a pointer to managed memory and assigns it a value + *outVector = GetVectorFromSomewhere(); + } + ``` + +--- + +## The Bindings Generator + diff --git a/mkdocs.yml b/mkdocs.yml index d0e62a5..b05259d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,6 +80,7 @@ nav: - Naming Conventions: style/naming-conventions.md - Tooling: - Overview: tooling/index.md + - Bindings Generator: tooling/bindings-generator.md copyright: | © 2026-present Intricate Dev Team From b3c7edd54c344fd010b7f229167bc46654d48992 Mon Sep 17 00:00:00 2001 From: Adam Foflonker <87928802+DnA-IntRicate@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:08:27 +0200 Subject: [PATCH 4/6] Write Bindings Generator section --- docs/tooling/bindings-generator.md | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/tooling/bindings-generator.md b/docs/tooling/bindings-generator.md index b87b379..7dde3b9 100644 --- a/docs/tooling/bindings-generator.md +++ b/docs/tooling/bindings-generator.md @@ -142,3 +142,42 @@ Non-PODs must be returned as **out pointers**. ## The Bindings Generator +The **Bindings Generator** is a code generator that automatically generates `C++ -> C#` **interop binding pairs** during build-time. It is written in `C#` and is integrated into **Premake** such that it runs before `IntricateEngine` & `IntricateEngine.NET` are compiled during builds. This ensures that the **interop bindings** are always up-to-date. + +The Bindings Generator uses a series of **Interface Definition Language (IDL)** files written in **JSONC** as input. The IDL schema used is as follows: + +``` jsonc +{ + "Metadata": { + "Type": "TypeName", + "Headers": [ + "IntricateEngine/Header1.hpp", + "IntricateEngine/Header2.hpp" + ], + "Source": "IntricateEngine/SourceFile.cpp" + }, + "Functions": { + "TypeName_FuncName": { + "FuncSigType": "FetchExecute", + "Params": "(void* param, NativeID objID)", + "ParamsT": [ + { + "Type": "void*", + "Name": "param" + }, + { + "Type": "NativeID", + "NativeType": "NativeObjectType", // OPTIONAL: NativeType only needs to be defined if Type = "NativeID" + "Name": "objID" + } + ], + "Return": "void" + }, + // Rest of the binding functions... + } +} +``` + +### Function Signature Types + +--- From 5914d4110810a7d73dc0386f05e11b08556da760 Mon Sep 17 00:00:00 2001 From: Adam Foflonker <87928802+DnA-IntRicate@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:08:57 +0200 Subject: [PATCH 5/6] Document all FuncSigTypes --- docs/tooling/bindings-generator.md | 311 ++++++++++++++++++++++++++++- 1 file changed, 308 insertions(+), 3 deletions(-) diff --git a/docs/tooling/bindings-generator.md b/docs/tooling/bindings-generator.md index 7dde3b9..5005527 100644 --- a/docs/tooling/bindings-generator.md +++ b/docs/tooling/bindings-generator.md @@ -144,9 +144,9 @@ Non-PODs must be returned as **out pointers**. The **Bindings Generator** is a code generator that automatically generates `C++ -> C#` **interop binding pairs** during build-time. It is written in `C#` and is integrated into **Premake** such that it runs before `IntricateEngine` & `IntricateEngine.NET` are compiled during builds. This ensures that the **interop bindings** are always up-to-date. -The Bindings Generator uses a series of **Interface Definition Language (IDL)** files written in **JSONC** as input. The IDL schema used is as follows: +The Bindings Generator uses a series of **Interface Definition Language (IDL)** files written in `jsonc` as input. The IDL schema used is as follows: -``` jsonc +``` json { "Metadata": { "Type": "TypeName", @@ -178,6 +178,311 @@ The Bindings Generator uses a series of **Interface Definition Language (IDL)** } ``` -### Function Signature Types +### Function signature types + +The Bindings Generator emits a fixed set of native and managed function shapes depending on the ownership model, return type, and call target. The generator has a list of **function signatures** that are used to define the `FuncSigType` field seen in the `jsonc` schema earlier. Each **function signature** determines the shape of the generated binding pair. + +!!! warning + It is of **critical importance** that the `FuncSigType` used for a particular binding pair is correct, otherwise compile-time errors and or crashes and undefined behaviour may occur. + + +!!! note + `...` represents a placeholder for zero or more ABI-safe parameters. + It does not indicate C-style variadic arguments. + +The following signatures document the canonical patterns generated by the system. + +#### NativeObjectCreate + +Used for `Create` functions when a new `NativeObject` needs to be created + +``` C++ +_IE_BINDING_QUALIFIER uint32 TypeName_Create(...) +{ + return Helpers::Create<_Ty>(...); +} +``` + +#### FetchExecute + +Executes an instance method if the native object is valid. + +``` C++ +_IE_BINDING_QUALIFIER void TypeName_Foo(NativeID nativeID, ...) +{ + Helpers::FetchExecuteIfValid(nativeID, [&](const Ref& x) + { + x->Foo(...); + }); +} +``` + +#### FetchReturn + +Executes an instance method with an ABI-safe return if the object is valid. + +``` C++ +_IE_BINDING_QUALIFIER ReturnType TypeName_GetFunc(NativeID nativeID, ...) +{ + return Helpers::FetchExecuteIfValid(nativeID, [&](const Ref& x) + { + return x->GetFunc(...); + }); +} +``` + +#### FetchReturnOut + +Executes an instance method with an out-pointer return if the object is valid. + +``` C++ +_IE_BINDING_QUALIFIER void TypeName_GetFunc(NativeID nativeID, ..., ReturnType* outVal) +{ + *outVal = Helpers::FetchExecuteIfValid(nativeID, [&](const Ref& x) + { + return x->GetFunc(...); + }); +} +``` + +#### FetchReturnOutField + +Returns a pointer to a native object's field as an out-pointer return if the object is valid. + +``` C++ +_IE_BINDING_QUALIFIER void TypeName_GetFieldNamePtr(NativeID nativeID, ReturnType** outVal) +{ + *outVal = Helpers::FetchExecuteIfValid(nativeID, [&](const Ref& x) + { + return &x->FieldName; + }); +} +``` + +#### FetchReturnNativeObject + +Executes an instance method returning the ID to a NativeObject if this object is valid. + +``` C++ +_IE_BINDING_QUALIFIER uint32 TypeName_GetFunc(NativeID nativeID, ...) +{ + return Helpers::FetchExecuteIfValid(nativeID, [&](const Ref& x) + { + return (uint32)x->GetFunc(...)->GetNativeID(); + }); +} +``` + +#### FetchReturnString + +Executes an instance method returning a string as an ABI-safe `void*` if the object is valid. + +``` C++ +_IE_BINDING_QUALIFIER void* TypeName_GetString(NativeID nativeID, ...) +{ + return Helpers::FetchExecuteIfValid(nativeID, [&](const Ref& x) + { + return (void*)x->GetString(...).data(); + }); +} +``` + +!!! warning + The `void*` returned here is not stable, so managed code must copy the contents of the string into managed memory as soon as possible when using this. + +#### FetchReturnPinned + +Executes an instance method returning a complex data type that must be "pinned" in native heap memory to extend it's lifetime long enough for managed code to copy it. + +``` C++ +_IE_BINDING_QUALIFIER Memory::PinnedBlock TypeName_GetFunc(NativeID nativeID, ...) +{ + return Memory::Pin(Helpers::FetchExecuteIfValid(nativeID, [&](const Ref& x) + { + return x->GetFunc(...); + })); +} +``` + +#### StaticExecute + +Executes a static method. + +``` C++ +_IE_BINDING_QUALIFIER void TypeName_Foo(...) +{ + TypeName::Foo(...); +} +``` + +#### StaticReturn + +Executes a static method with an ABI-safe return. + +``` C++ +_IE_BINDING_QUALIFIER ReturnType TypeName_Foo(...) +{ + return TypeName::Foo(...); +} +``` + +#### StaticReturnOut + +Executes a static method with an out-pointer return. + +``` C++ +_IE_BINDING_QUALIFIER ReturnType TypeName_Foo(..., ReturnType* outVal) +{ + *outVal = TypeName::Foo(...); +} +``` + +#### StaticReturnPinned + +Executes a static method returning a complex data type that must be "pinned" in native heap memory to extend it's lifetime long enough for managed code to copy it. + +``` C++ +_IE_BINDING_QUALIFIER Memory::PinnedBlock TypeName_Foo(...) +{ + return Memory::Pin(TypeName::Foo(...)); +} +``` + +#### SingletonExecute + +Executes a static method on a singleton. + +``` C++ +_IE_BINDING_QUALIFIER void TypeName_Foo(...) +{ + TypeName::Get().Foo(...); +} +``` + +#### SingletonReturn + +Executes a static method on a singleton with an ABI-safe return. + +``` C++ +_IE_BINDING_QUALIFIER ReturnType TypeName_Foo(...) +{ + return TypeName::Get().Foo(...); +} +``` + +#### SingletonReturnPinned + +Executes a static method on a singleton returning a complex data type that must be "pinned" in native heap memory to extend it's lifetime long enough for managed code to copy it. + +``` C++ +_IE_BINDING_QUALIFIER Memory::PinnedBlock TypeName_Foo(...) +{ + return Memory::Pin(TypeName::Get().Foo(...)); +} +``` + +#### PointerCastExecute + +Executes an instance method on a raw `this` pointer passed from managed code. + +``` C++ +_IE_BINDING_QUALIFIER void TypeName_Foo(void* ptr, ...) +{ + _IE_CORE_ASSERT(ptr, "ptr from managed was null!"); + (static_cast(ptr))->Foo(...); +} +``` + +#### PointerCastReturn + +Executes an instance method with an ABI-safe return on a raw `this` pointer passed from managed code. + +``` C++ +_IE_BINDING_QUALIFIER ReturnType TypeName_Foo(void* ptr, ...) +{ + _IE_CORE_ASSERT(ptr, "ptr from managed was null!"); + return (static_cast(ptr))->Foo(...); +} +``` + +#### EcsGetNativeObject + +Returns the ID to a NativeObject field of a component part of the **entity-component-system**. + +``` C++ +_IE_BINDING_QUALIFIER uint32 ComponentName_GetFieldName(NativeID sceneID, uint32 entityID) +{ + return Helpers::GetComponentNativeObjectField(sceneID, entityID); +} +``` + +!!! note + The `FieldName` must be the same as the `TypeName` here. + +#### EcsGetField + +Returns an ABI-safe field of a component part of the **entity-component-system**. + +``` C++ +_IE_BINDING_QUALIFIER ReturnType ComponentName_GetFieldName(NativeID sceneID, uint32 entityID) +{ + return Helpers::GetComponentField(sceneID, entityID); +} +``` + +#### EcsGetOutField + +Returns a field of a component part of the **entity-component-system** as an out-pointer. + +``` C++ +_IE_BINDING_QUALIFIER void ComponentName_GetFieldName(NativeID sceneID, uint32 entityID, ReturnType* outVal) +{ + Helpers::GetComponentField(sceneID, entityID, outVal); +} +``` + +#### EcsGetStringField + +Returns a string field of a component part of the **entity-component-system** as an out-void-pointer. + +``` C++ +_IE_BINDING_QUALIFIER void ComponentName_GetFieldName(NativeID sceneID, uint32 entityID, void** outStrPtr) +{ + Helpers::GetComponentField(sceneID, entityID, outStrPtr); +} +``` + +#### EcsSetField + +Sets an ABI-safe field of a component part of the **entity-component-system**. + +``` C++ +_IE_BINDING_QUALIFIER void ComponentName_SetFieldName(NativeID sceneID, uint32 entityID, TypeName val) +{ + Helpers::SetComponentField(sceneID, entityID, val); +} +``` + +#### EcsSetStringField + +Sets a string field of a component part of the **entity-component-system**. + +``` C++ +_IE_BINDING_QUALIFIER void ComponentName_SetFieldName(NativeID sceneID, uint32 entityID, ManagedUTF8String val) +{ + Helpers::SetComponentField(sceneID, entityID, val); +} +``` + +#### MacroExecute + +Executes functionality defined in a `C++` macro. + +``` C++ +_IE_BINDING_QUALIFIER void TypeName_Foo(...) +{ + _IE_FOO(...); +} +``` --- From ffd32f239636ea97c63d5c972003eb544d555a45 Mon Sep 17 00:00:00 2001 From: Adam Foflonker <87928802+DnA-IntRicate@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:14:39 +0200 Subject: [PATCH 6/6] chore: Fix md linting issues --- docs/tooling/bindings-generator.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/tooling/bindings-generator.md b/docs/tooling/bindings-generator.md index 5005527..3ee5482 100644 --- a/docs/tooling/bindings-generator.md +++ b/docs/tooling/bindings-generator.md @@ -49,7 +49,7 @@ As we can see from the above example, the **unmangled** symbol is much easier to [LibraryImport(LibIntricateEngine, EntryPoint = "RenderCommand_GetAPI")] ``` -Using **unmangled** symbols do however present us with a problem: all exported symbols must now be **unique**. This means no two functions can have the same name - including overloaded functions. This is the reason why we write **binding function names** in the format: `TypeName_FunctionName` - this ensures the uniqueness of the symbol. +Using **unmangled** symbols do however present us with a problem: all exported symbols must now be **unique**. This means no two functions can have the same name - including overloaded functions. This is the reason why we write **binding function names** in the format: `TypeName_FunctionName` - this ensures the uniqueness of the symbol. ### But how do we handle overloads? @@ -86,7 +86,7 @@ This agreement covers, among other things: - Which registers are used for parameters and return values - Stack usage and stack-frame layout -And as such, all interop binding functions we write must also adhere to ABI requirements - otherwise everything turns into **undefined behaviour**. +And as such, all interop binding functions we write must also adhere to ABI requirements - otherwise everything turns into **undefined behaviour**. !!! note In Intricate, on the `x86_64` platform, the **Microsoft x64 ABI** is used. @@ -185,7 +185,6 @@ The Bindings Generator emits a fixed set of native and managed function shapes d !!! warning It is of **critical importance** that the `FuncSigType` used for a particular binding pair is correct, otherwise compile-time errors and or crashes and undefined behaviour may occur. - !!! note `...` represents a placeholder for zero or more ABI-safe parameters. It does not indicate C-style variadic arguments. @@ -416,7 +415,7 @@ _IE_BINDING_QUALIFIER uint32 ComponentName_GetFieldName(NativeID sceneID, uint32 } ``` -!!! note +!!! note The `FieldName` must be the same as the `TypeName` here. #### EcsGetField