This document describes how the C++/C# interop bridge works in Chained Engine, how the Roslyn Source Generator (Chained.Managed.Generator) automates native bindings, and how to expose new native C++ component functions to C# gameplay scripts.
Chained Engine uses Coral — a C++ wrapper around .NET CoreCLR — for C++/C# interoperability.
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ Native C++ Engine │ │ Managed C# Scripts │
│ │ │ │
│ script_glue_*.cpp │ │ src/Components/*.cs │
│ extern "C" C++ functions │ │ [NativeProperty] / │
│ │ │ │ [NativeCall] attributes │
│ │ │ │ │ │
│ │ (Function Pointers) │ │ │ (Roslyn Code Gen) │
│ ▼ │ │ ▼ │
│ Coral::Assembly │ │ .g.cs (Generated File) │
│ AddInternalCall("Class", │ ═══════>│ internal static unsafe │
│ "Method_Ptr", &C++Fn) │ Writes │ delegate* unmanaged<...> │
│ UploadInternalCalls() │ Pointer │ Method_Ptr; │
└────────────────────────────────┘ └────────────────────────────────┘
- C++ Glue Functions: Declared as
extern "C"(using macroCH_SCRIPT_FUNC). - C++ Registration:
ScriptGlue::RegisterInternalCalls()binds C++ function pointers to string names (ClassName.MethodName_Ptr). - C# Roslyn Generator:
NativeCallGeneratorscans[NativeCall]and[NativeProperty]attributes at compile time and auto-generatesdelegate* unmanaged<...>function pointer fields and C# property getters/setters. - Coral Binding: At assembly load time, Coral matches string names against static
_Ptrfields in C# assemblies and writes C++ function pointers directly into those fields.
The generator project lives in scripting/managed/Chained.Managed.Generator/ and compiles to a Roslyn analyzer DLL (Chained.Managed.Generator.dll).
Declares a single native function binding.
[NativeCall("Chained.AnimationComponent", "AnimationComponent_CrossFade", "void", "ulong", "int", "float")]
public partial class AnimationComponent : Component { ... }- Signature format:
[returnType, param1, param2, ...] - First parameter: Always entity ID (
ulong) for component accessors. - Auto-generates:
internal static unsafe delegate* unmanaged<ulong, int, float, void> AnimationComponent_CrossFade_Ptr;
Declares a full C# property getter/setter AND generates the corresponding _Ptr fields.
[NativeProperty("MovementSpeed", "float", "PlayerComponent_GetMovementSpeed", "PlayerComponent_SetMovementSpeed")]
[NativeProperty("IsKinematic", "bool", "RigidBody_IsKinematic", "RigidBody_SetKinematic")]
[NativeProperty("Translation", "Vector3", "Transform_GetTranslation", "Transform_SetTranslation")]
public partial class PlayerComponent : Component { ... }-
Auto-generates:
- Both
Get_PtrandSet_Ptrunmanaged function pointer fields. - The full C# property getter/setter with null-checks,
unsafeblocks, and type marshaling (bool$\leftrightarrow$ byte,Vector3*out-pointer).
- Both
The C++ glue functions and C# generator follow strict ABI type mapping rules:
| Logical Type | C++ Type (script_glue_*.cpp) |
C# Attribute String | Generated C# Type / Pointer |
|---|---|---|---|
| Entity ID | uint64_t |
"ulong" |
ulong |
| Boolean | uint8_t |
"bool" / "byte" |
byte ((byte)(value ? 1 : 0)) |
| Integer | int32_t / int |
"int" |
int |
| Unsigned Int | uint32_t |
"uint" |
uint |
| Float | float |
"float" |
float |
| Double | double |
"double" |
double |
| UTF-16 String | Coral::UCChar* / char16_t* |
"char*" |
char* |
| UTF-16 String (property) | const Coral::UCChar* / std::string |
"string" |
Marshal.PtrToStringUni(new IntPtr(...)) |
| Vector2 Struct | glm::vec2* |
"Vector2" / "Vector2*" |
Chained.Vector2* (out-pointer) |
| Vector3 Struct | glm::vec3* |
"Vector3" / "Vector3*" |
Chained.Vector3* (out-pointer) |
| Vector4 Struct | glm::vec4* |
"Vector4" / "Vector4*" |
Chained.Vector4* (out-pointer) |
⚠️ Important: Structs (Vector2,Vector3,Vector4) are always passed by pointer across the ABI boundary to ensure stack alignment and prevent platform ABI discrepancies.
For simple property get/set, you don't need to write any C++ glue code. The tools/generate_glue.py script scans C# [NativeProperty] attributes and generates everything automatically.
C# [NativeProperty] Python generator C++ build
attribute ───> tools/generate_glue.py ───> auto-compiled
│
CMakeLists.txt --classes ──────────────────────────────┘
tools/generate_glue.pyreadsengine/scripting/managed/src/Components/*.cs- Filters by
--classesparameter (e.g.PlayerComponent SpawnComponent) - Generates three files in
engine/scripting/generated/:script_glue_generated.h—CH_SCRIPT_FUNCdeclarationsscript_glue_generated.cpp— getter/setter implementationsscript_glue_generated_reg.inl—AddInternalCallregistrations (included byscript_glue.cpp)
- CMake custom command runs the generator automatically when C# sources change
Add your class to the --classes list in engine/scripting/CMakeLists.txt:
COMMAND Python3::Interpreter "${GLUE_GENERATOR_SCRIPT}"
--cs-dir "${MANAGED_PROJECT_DIR}/src/Components"
--output-dir "${GLUE_OUTPUT_DIR}"
--classes PlayerComponent SpawnComponent NetworkIdentityComponent YourComponentThen build normally — the generator runs as part of the build.
The generator handles simple field access and string properties automatically. It does NOT handle:
- Physics synchronization (e.g. updating Jolt body when translation changes)
- Complex logic (e.g. finding entities by tag, conditional behavior)
For these cases, use [NativeCall] and write hand-written glue in script_glue_*.cpp — see the manual tutorial below.
String support: [NativeProperty] with "string" type auto-generates Coral::UCChar* getters (via GlueStringPool::ReturnString) and setters (via ch_u16_to_string).
[NativeCall] stubs: The generator also emits C++ stub functions for [NativeCall] attributes. These are placeholders — the actual implementations should be hand-written in script_glue_*.cpp files which override the stubs at link time.
Suppose you want to expose a new Stamina property on PlayerComponent.
Step 1: Add [NativeProperty] to C# Component
In scripting/managed/src/Components/PlayerComponent.cs:
namespace Chained
{
[NativeProperty("MovementSpeed", "float", "PlayerComponent_GetMovementSpeed", "PlayerComponent_SetMovementSpeed")]
[NativeProperty("Stamina", "float", "PlayerComponent_GetStamina", "PlayerComponent_SetStamina")]
public partial class PlayerComponent : Component
{
}
}Step 2: Register the class in engine/scripting/CMakeLists.txt:
--classes PlayerComponent SpawnComponent NetworkIdentityComponentStep 3: Build:
cmake --build --preset windows-clang-debug --parallelDone. The generator creates the C++ getter, setter, and registration automatically. The Roslyn Source Generator on the C# side creates the _Ptr fields and property body.
If your property needs physics sync, string conversion, or other complex behavior, write the C++ glue by hand.
Step 1: Implement C++ Glue Function
In scripting/script_glue_player.cpp:
CH_SCRIPT_FUNC float PlayerComponent_GetStamina(uint64_t entityID)
{
Entity entity = GetEntity(entityID);
if (entity && entity.HasComponent<PlayerComponent>())
return entity.GetComponent<PlayerComponent>().Stamina;
return 0.0f;
}
CH_SCRIPT_FUNC void PlayerComponent_SetStamina(uint64_t entityID, float stamina)
{
Entity entity = GetEntity(entityID);
if (entity && entity.HasComponent<PlayerComponent>())
entity.GetComponent<PlayerComponent>().Stamina = stamina;
}Step 2: Register in script_glue.cpp
In scripting/script_glue.cpp under ScriptGlue::RegisterInternalCalls():
assembly.AddInternalCall("Chained.PlayerComponent", "PlayerComponent_GetStamina_Ptr", (void*)&PlayerComponent_GetStamina);
assembly.AddInternalCall("Chained.PlayerComponent", "PlayerComponent_SetStamina_Ptr", (void*)&PlayerComponent_SetStamina);Step 3: Declare in header
In scripting/script_glue_entity.h (or the relevant script_glue_*.h):
CH_SCRIPT_FUNC float PlayerComponent_GetStamina(uint64_t entityID);
CH_SCRIPT_FUNC void PlayerComponent_SetStamina(uint64_t entityID, float stamina);Step 4: Add [NativeProperty] to C# Component
Same as the automatic path — the C# Roslyn generator needs the attribute to create the _Ptr fields and property body.
For methods that take parameters beyond simple field access (e.g. Play(), Stop(), CrossFade(int, float)), use [NativeCall]:
Step 1: Add [NativeCall] to C# Component
[NativeCall("Chained.AudioComponent", "AudioComponent_Play", "void", "ulong")]
[NativeCall("Chained.AudioComponent", "AudioComponent_Stop", "void", "ulong")]
[NativeCall("Chained.AnimationComponent", "AnimationComponent_CrossFade", "void", "ulong", "int", "float")]
public partial class AudioComponent : Component
{
public void Play()
{
unsafe { if (AudioComponent_Play_Ptr != null) AudioComponent_Play_Ptr(Entity.ID); }
}
}- Format:
[NativeCall("Namespace.Class", "FunctionName", "ReturnType", "param1Type", "param2Type", ...)] - First parameter is always
ulong(entity ID) - The generator creates
_Ptrfields and emits C++ stub functions - You still write the C# wrapper method and the C++ implementation manually
Step 2: Implement C++ glue in script_glue_*.cpp:
CH_SCRIPT_FUNC void AudioComponent_Play(uint64_t entityID)
{
Entity entity = GetEntity(entityID);
if (entity && entity.HasComponent<AudioComponent>())
entity.GetComponent<AudioComponent>().Play();
}Registration is handled automatically by generate_glue.py.
- Viewing Generated Code: During compilation with MSBuild (
/p:EmitCompilerGeneratedFiles=true), generated C# files are saved underscripting/managed/obj/GeneratedFiles/Chained.Managed.Generator/. - Component Must Be
partial: Any C# class decorated with[NativeCall]or[NativeProperty]must have thepartialkeyword. - Null Safety: All generated properties contain defensive
!= nullchecks on the_Ptrfields. If a function is not registered on the C++ side, the property getter will returndefaultinstead of crashing with aNullReferenceException.