This page covers the C++ side of Godot's C# scripting integration: how the CSharpLanguage singleton registers as a script language, how GDMono loads and initializes the .NET runtime, how CSharpScript and CSharpInstance manage the per-script and per-object lifecycle, how hot-reloading works in editor builds, and how native Godot objects acquire managed C# bindings.
For the C#-side interop bridge and function-pointer mechanism that allows C++ to invoke managed code, see 13.3. For the BindingsGenerator that produces the Godot.* C# API wrappers, see 13.2.
The C# scripting module lives entirely under modules/mono/. It is disabled by default and must be enabled with module_mono_enabled=yes at build time (modules/mono/config.py34-36).
| Subdirectory | Role |
|---|---|
modules/mono/ | Root: CSharpLanguage, CSharpScript, CSharpInstance |
modules/mono/mono_gd/ | GDMono runtime init, GDMonoCache callback table |
modules/mono/glue/ | C++ interop glue; also contains the GodotSharp C# assembly source |
modules/mono/glue/GodotSharp/.../Bridge/ | Managed side: ScriptManagerBridge, ManagedCallbacks |
modules/mono/editor/ | BindingsGenerator, editor tooling (tools-only) |
modules/mono/build_scripts/ | SCons helper mono_configure.py |
GD_MONO_HOT_RELOAD is defined only in editor builds via mono_configure.py (modules/mono/build_scripts/mono_configure.py19-22).
Diagram: Major C++ Classes and Their Relationships
Sources: modules/mono/csharp_script.h58-600 modules/mono/mono_gd/gd_mono.h60-182
CSharpLanguage is the singleton that Godot's ScriptServer queries for anything related to .cs files. It owns the GDMono instance and coordinates the full script lifecycle.
CSharpLanguage::init() is called during engine startup (modules/mono/csharp_script.cpp101-133):
dotnet/project/assembly_name, reload attempt count)._editor_init_callback to run after the editor is set up.GDMono instance via memnew(GDMono).gdmono->initialize() only if should_initialize() returns true (always in editor; in the game player only if the dotnet feature tag is present).CSharpLanguage::frame() calls ScriptManagerBridge_FrameCallback every frame to tick the managed Dispatcher task scheduler (modules/mono/csharp_script.cpp534-538).
CSharpLanguage::finalize() (modules/mono/csharp_script.cpp139-189):
DisposablesTracker_OnGodotShuttingDown so managed disposables can clean up.CSharpScriptBinding GC handles and detaches them from their owner objects.GDMono instance.script_bindings.When the editor finishes loading, _editor_init_callback (modules/mono/csharp_script.cpp1072-1092) uses GDMono::get_plugin_callbacks().LoadToolsAssemblyCallback to load GodotTools.dll, which is the managed editor plugin. That plugin object is then added to EditorNode via EditorNode::add_editor_plugin.
Sources: modules/mono/csharp_script.cpp101-133 modules/mono/csharp_script.cpp534-538 modules/mono/csharp_script.cpp139-189 modules/mono/csharp_script.cpp1072-1092
GDMono owns the interaction with the native .NET host. Its initialize() method tries several loading strategies in order (modules/mono/mono_gd/gd_mono.cpp636-760).
Diagram: GDMono Runtime Loading Decision Tree
Sources: modules/mono/mono_gd/gd_mono.cpp258-675
godot_plugins_initialize DoesThe managed entry point (GodotPlugins.Main.InitializeFromEngine in editor, GodotPlugins.Game.Main.InitializeFromGameProject in game) receives:
is_editor.PluginCallbacks* (editor only) — filled with function pointers for loading assemblies.ManagedCallbacks* — filled with the complete managed callback table.godotsharp::get_runtime_interop_funcs).After this call, GDMonoCache::managed_callbacks is populated and GDMonoCache::godot_api_cache_updated is set to true.
Sources: modules/mono/mono_gd/gd_mono.cpp636-760 modules/mono/mono_gd/gd_mono_cache.cpp40-99
GDMonoCache::ManagedCallbacks (modules/mono/mono_gd/gd_mono_cache.h70-164) is a plain struct of function pointers. Each pointer is a [UnmanagedCallersOnly] method on the managed side.
The C# mirror is Godot.Bridge.ManagedCallbacks (modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs8-99).
Diagram: ManagedCallbacks — Key Callback Groups and Code Locations
Sources: modules/mono/mono_gd/gd_mono_cache.h70-164 modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs1-99
| Callback Group | Purpose |
|---|---|
ScriptManagerBridge_* | Script/type registration, object binding creation, frame tick |
CSharpInstanceBridge_* | Property get/set, method calls, dispose, serialize/deserialize |
GCHandleBridge_* | Free or query GC handles |
DelegateUtils_* | Invoke/compare/serialize C# delegates (for ManagedCallable) |
SignalAwaiter_SignalCallback | Resume await ToSignal(...) tasks |
DisposablesTracker_OnGodotShuttingDown | Engine shutdown cleanup |
DebuggingUtils_GetCurrentStackInfo | Stack traces during debugger breaks |
GD_OnCoreApiAssemblyLoaded | Fires once when the core API assembly is ready |
CSharpScript extends Script and represents a single .cs source file. Its core state is a TypeInfo struct:
CSharpScript::TypeInfo {
String class_name
StringName native_base_name
String icon_path
bool is_tool
bool is_global_class
bool is_abstract
bool is_constructed_generic_type
bool is_generic_type_definition
}
(modules/mono/csharp_script.h65-126)
update_script_class_info() populates TypeInfo by calling ScriptManagerBridge_UpdateScriptClassInfo which reflects the managed type. This is called when a script reloads.
instance_create(Object* p_this) calls ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance, which on the managed side (modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs113-170) finds the script's Type via _scriptTypeBiMap, gets a constructor, allocates the managed object uninitialized, and invokes the constructor with p_this set as NativePtr.
CSharpScript cannot be used in built-in mode (supports_builtin_mode() returns false) and files must be named in PascalCase.
Sources: modules/mono/csharp_script.h58-305 modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs113-170
CSharpInstance implements ScriptInstance. Each instance holds a MonoGCHandleData gchandle — a strong or weak GC handle that keeps the corresponding managed object alive (or tracks it for reference counting).
Diagram: CSharpInstance Property/Method Call Flow
Sources: modules/mono/csharp_script.h307-386 modules/mono/mono_gd/gd_mono_cache.h98-99
CSharpInstance carefully manages reference counts for RefCounted-derived owners:
_reference_owner_unsafe() — bumps the refcount to keep the object alive while the managed object exists._unreference_owner_unsafe() — decrements refcount; if it hits zero, signals the caller to memdelete the owner.mono_object_disposed_baseref() is called from managed code when the C# object is GC'd, allowing the native side to decide whether to delete the owner.Sources: modules/mono/csharp_script.h323-329
When a Godot Object is accessed from C# without having a CSharpScript attached (e.g., GetNode<Camera3D>()), it still needs a managed wrapper. This is handled by instance bindings.
CSharpLanguage::get_instance_binding(Object* p_object) (modules/mono/csharp_script.cpp1171) stores a CSharpScriptBinding in script_bindings keyed by the object pointer. If one doesn't exist, it calls setup_csharp_script_binding() which:
ScriptManagerBridge_CreateManagedForGodotObjectBinding(&type_name, p_object) to create the C# wrapper.CSharpScriptBinding.gchandle.The _instance_binding_callbacks static (modules/mono/csharp_script.h446) is a GDExtensionInstanceBindingCallbacks struct passed to Object::set_instance_binding. It handles:
_instance_binding_create_callback — lazy-creates the binding._instance_binding_free_callback — releases the GC handle._instance_binding_reference_callback — manages weak/strong handle swapping for RefCounted objects.Sources: modules/mono/csharp_script.cpp1136-1300 modules/mono/csharp_script.h442-446
Hot-reload is only compiled into editor builds (#ifdef GD_MONO_HOT_RELOAD). It is triggered when the project assembly file's modification time changes.
Diagram: CSharpLanguage::reload_assemblies() Sequence
Sources: modules/mono/csharp_script.cpp624-1029
CSharpScript::StateBackupEach reloadable script instance's state is saved into a StateBackup (modules/mono/csharp_script.h155-161):
StateBackup {
List<Pair<StringName, Variant>> properties
Dictionary event_signals
}
This is populated via CSharpInstanceBridge_SerializeState (calls the managed ISerializationListener.OnBeforeSerialize) and restored via CSharpInstanceBridge_DeserializeState (calls OnAfterDeserialize).
ScriptManagerBridge ALC TrackingOn the C# side, ScriptManagerBridge tracks each registered type against its AssemblyLoadContext via _alcData (modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs22-71). When the ALC unloads, OnAlcUnloading fires and removes the types from _scriptTypeBiMap and _pathTypeBiMap, preserving enough data in _scriptDataForReload so that scripts without a file path can be reconnected after the new assembly loads.
Sources: modules/mono/csharp_script.h154-168 modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs22-71
ScriptManagerBridge)ScriptManagerBridge maintains two bidirectional maps (modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs):
| Map | Key | Value |
|---|---|---|
_scriptTypeBiMap | IntPtr scriptPtr (native CSharpScript*) | System.Type |
_pathTypeBiMap | string scriptPath | System.Type |
When CSharpLanguage::reload_assemblies() calls ScriptManagerBridge_AddScriptBridge(scr, &path), the managed side finds the Type for that path and registers it. If the path is csharp:// (a registered script without a file), TryReloadRegisteredScriptWithClass uses _scriptDataForReload to reconnect by assembly+class name.
Sources: modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs1-120 modules/mono/csharp_script.cpp842-883
Two CallableCustom subclasses handle C# signal/await integration (modules/mono/signal_awaiter_utils.cpp):
| Class | Purpose |
|---|---|
SignalAwaiterCallable | Used by await ToSignal(obj, "signal"). Holds a strong GC handle to a C# awaiter object; when the signal fires, calls SignalAwaiter_SignalCallback to resume the task. |
EventSignalCallable | Used to bridge C# event signals. When a C# event signal is raised, routes the Godot signal emission to the C# event. |
Sources: modules/mono/signal_awaiter_utils.cpp36-215
CSharpLanguage::init() registers these project settings:
| Setting | Default | Notes |
|---|---|---|
dotnet/project/assembly_name | "" | Name of the compiled game assembly |
dotnet/project/solution_directory | "" | Editor-only: where the .sln lives |
dotnet/project/assembly_reload_attempts | 3 | Editor-only: retry limit for hot reload |
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.