12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
LeanCLR-godot runs the LeanCLR interpreter inside Godot 4 as a GDExtension, so C# scripts (.cs) can be loaded and hot-reloaded by Godot without a Mono build. The bridge lets a C# class extend Godot.Node, and the engine instantiates it through the interpreter at runtime.
This is a hobby project (per the README) — not for production. Expect rough edges, no official bindings, and Godot 4.5+ as the target.
Build & Run
The README's steps are missing pieces. The full sequence:
# 1. Pull the LeanCLR git submodule.
git submodule update --init --recursive
# 2. Configure (also fetches godot-cpp 4.5 via FetchContent on first run).
cmake -S . -B build-master
# 3. Generate the Godot API bindings. extension_api.json is shipped inside
# the just-fetched godot-cpp tree, not at the repo root.
python3 tools/binding_generator/generate_bindings.py \
--api build-master/_deps/godot-cpp-src/gdextension/extension_api.json
# (on Windows use `python3` explicitly — `python` is aliased to the
# Microsoft Store stub and will pop the installer UI)
# 4. Re-configure now that src/generated/godot_api.generated.cpp exists,
# then build the native GDExtension.
cmake -S . -B build-master
cmake --build build-master --config Debug --target leanclr_godot
# 5. Build the managed C# facade and the demo (depends on step 4 + the
# facade's own output).
dotnet msbuild managed/GodotSharpCompat/GodotSharpCompat.csproj -p:Configuration=Debug
dotnet msbuild project/Game.csproj -p:Configuration=Debug
# 6. Run.
"/d/Projects/godot-build/build/godot.exe" --path project # adjust to your Godot install
Outputs:
project/bin/<Config>/leanclr_godot.{dll,so,dylib,wasm}— the GDExtension, loaded viaproject/leanclr.gdextension.project/leanclr/Game.dll,project/leanclr/GodotSharpCompat.dll— the C# assemblies LeanCLR loads at runtime.project/leanclr/{mscorlib,System,System.Core,...}.dll— LeanCLR's bundled .NET Framework 4.7.2 BCL, referenced byproject/Game.csprojandmanaged/GodotSharpCompat/GodotSharpCompat.csprojviaHintPath. They are not built here; they ship insidethirdparty/leanclr/src/libraries/dotnetframework4.x-linux/.
project/Game.csproj and managed/GodotSharpCompat/GodotSharpCompat.csproj both use legacy .NET Framework v4.7.2 with <NoStandardLib>true</NoStandardLib> and <NoConfig>true</NoConfig> (they link against LeanCLR's bundled mscorlib/System, not the host BCL). They emit a single DLL straight into project/leanclr/.
WASM build is supported (-sSIDE_MODULE=1, output .wasm in project/bin/Release/) — see CMakeLists.txt's EMSCRIPTEN branch.
Two Source Patches Required for godot-cpp 4.5
The GODOT_CPP_BRANCH was bumped from 4.4 → 4.5 (uncommitted change in CMakeLists.txt). Two 4.5 API changes break the as-published code; both are fixed in this working tree:
-
ClassDBwas renamed to aClassDBSingletonwrapper in godot-cpp 4.5. TheCPP_HEADER_NAME_OVERRIDES["ClassDB"] = "class_db_singleton"intools/binding_generator/generate_bindings.pyonly renames the include path; the C++ class symbol in generated code is stillClassDBand breaks compilation. Fix: added aSINGLETON_CLASS_CPP_NAMESmap in the generator that:- Maps
ClassDB→ClassDBSingletonin the C++ class name emitted in the generated native function. - Forces
is_static=Truefor all ClassDB methods (since 4.5 only exposes them as non-static on the singleton instance, accessed viaClassDBSingleton::get_singleton()). - Routes the singleton dispatch through
ClassDBSingleton::get_singleton()->method(...)in both the regular and vararggenerate_native_method_functionpaths. - The C# side correspondingly gets
staticmethods onClassDB(still extendingGodotObject, so existing type-checks pass).
- Maps
-
gdextension_interface_script_instance_create3moved in godot-cpp 4.5 from<godot_cpp/core/gdextension_interface_loader.hpp>(which no longer exists) tonamespace godot::internalinside<godot_cpp/godot.hpp>. Fix insrc/leanclr_script.cpp:- Replaced
#include <godot_cpp/core/gdextension_interface_loader.hpp>with#include <godot_cpp/godot.hpp>. - Changed the call site from
gdextension_interface::script_instance_create3(...)to::godot::internal::gdextension_interface_script_instance_create3(...).
- Replaced
One Patch Required Inside the LeanCLR Submodule
thirdparty/leanclr/ is a git submodule, so the patch lives outside this repo's history and won't survive a git submodule update. In src/runtime/build_config.h, the upstream default is:
#define LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1
Set it to 0. The binding generator emits ~5800 native icalls but the C# side only calls a tiny fraction; with the default 1, hitting an unwired NotImplemented site calls fatal_on_not_implemented_error() and aborts the process (see LEANCLR_CODEGEN_RETURN_NOT_IMPLEMENTED_ERROR in codegen/leanclr_common.h and RETURN_NOT_IMPLEMENTED_ERROR in core/rt_base.h). With 0, the interpreter returns RtErr::NotImplemented and execution continues, which is what the demo needs to be runnable today.
This patch must be reapplied after every git submodule update --init --recursive (or after pulling new commits into the submodule).
Repository Layout
src/— GDExtension C++ (one .h/.cpp per component, see below)managed/GodotSharpCompat/— C# façade: hand-writtenGD,Node,GodotObject,ExportAttribute,Variant/marshalling helpers,CallableDelegateRegistry; plus aGenerated/subdir produced by the binding generator (gitignored — must be regenerated).project/— the Godot project itself (.gdextension,.csproj, scenes, scripts,leanclr/output dir,bin/for the GDExtension binaries)tools/binding_generator/generate_bindings.py— Python generator for bothsrc/generated/godot_api.generated.cppandmanaged/GodotSharpCompat/Generated/*.generated.csthirdparty/leanclr/— LeanCLR git submodule, pulled viaadd_subdirectory(${LEANCLR_ROOT}/src/runtime leanclr_runtime)in the top-level CMakegdextension_interface.h— pinned Godot 4 GDExtension interface header (the matching headers used by the binding generator live inside the fetched godot-cpp tree under_deps/.../gdextension/)
Native Bridge Architecture (src/)
The GDExtension wires LeanCLR into Godot through six cooperating pieces, registered at MODULE_INITIALIZATION_LEVEL_SCENE in register_types.cpp:
LeanCLRRuntimeBridge(leanclr_runtime_bridge.{h,cpp}) — the only file that knows about both godot-cpp and LeanCLR. Owns the globalLeanCLRVirtualMachine, loads assemblies by name from the configured assembly directory, creates/release managed objects, and exposes overloadedinvoke_script_method/get_script_property/set_script_propertyfor the typed value marshallers LeanCLR needs. Also handles owner→managed-object registration andmigrate_script_statefor hot reload.LeanCLRScriptLanguage(leanclr_script_language.{h,cpp}) —ScriptLanguageExtensionsingleton. Tells Godot there's a "LeanCLR" language; provides reserved words, validation, completion, debug hooks (most are stubs), and_reload_*entry points._reload_all_scripts/_reload_scriptsare what the runtime editor hits.LeanCLRScript(leanclr_script.{h,cpp}) —ScriptExtensionfor an individual.cs/.lcsfile.parse_source()extractsassembly_name,type_name, optionalentry_point, andbase_type(defaults to"Node") from header comments via a simple parser in this file. Callsgodot::internal::gdextension_interface_script_instance_create3to allocate script instances.LeanCLRScriptLoader/LeanCLRScriptSaver(leanclr_script_{loader,saver}.{h,cpp}) —ResourceFormatLoader/Saverpair. Recognizescsandlcsextensions, hands aLeanCLRScriptresource to Godot so.csfiles can be attached to nodes and saved in.tscn/.tres.LeanCLRMain(leanclr_main_node.{h,cpp}) — aNodethat, onNOTIFICATION_READY, instantiates the configured C# class (defaultGame.ClassDbMain) through the bridge. Used bymain.tscnas the bootstrapping demo node.LeanCLRHotReloadHost(leanclr_hot_reload_host.{h,cpp}) — aNodethat owns the current managed object for runtime hot reload. Wires_Input/process through the bridge, and onreload_assemblycaptures state viaCaptureHotReloadState(customVariantblob from the script), instantiates the new assembly, callsmigrate_script_state, thenRestoreHotReloadStateandOnHotReloadedon the new instance. Skips itself while in the editor or while a scene is being edited.leanclr_wasm_libcxx_shim.cpp— minimalstd::__1::__hash_memoryshim for Emscripten (libc++ is missing the symbol LeanCLR's stdlib expects when linking into a wasm side module).src/generated/godot_api.generated.cpp— auto-generated, ~5800+ per-class marshalling functions. Do not edit; regenerate via the binding generator.
Demo / Hot Reload Flow
project/runtime_hot_reload_demo.tscn is the main scene and exercises the hot-reload path end to end:
FlappyScript(aLeanCLRScriptpointing atproject/scripts/HotReloadSmoke.cs) is the gameplay node.LiveHotReloadHost(aLeanCLRHotReloadHost) is the bridge that drives the C# object's lifecycle.HotReloadInputRelay.gdforwards input and pollsres://leanclr/live_reload.txteveryreload_poll_seconds. When the marker changes from the defaultGame, it callsreload_assemblyon the host.RuntimeCSharpEditor.gdis aCodeEditwindow inside the game. Pressing Run writesHotReloadSmoke.csback, shells out todotnet msbuildto produce a uniquely-namedGameRuntimeEdit<unix_ms>.dllintoproject/leanclr/, then writes that name tolive_reload.txtso the relay picks it up on its next tick. It also rebuilds the defaultGame.dllafterward and restores the runtime-edit dll fromuser://so the next editor run is reproducible.HotReloadSmoke.csimplementsCaptureHotReloadState/RestoreHotReloadState/OnHotReloadedso the bird/physics state survives across reloads. Setting the env varLEANCLR_RUNTIME_EDITOR_AUTORUNtriggers a one-shot edit-and-rebuild on startup to verify the path.
Conventions & Gotchas
- The C#
GodotSharpCompatshim has aGenerated/subdir in its.gitignore; if you regenerate bindings keep it out of the repo. - The native bridge is large (
leanclr_runtime_bridge.cppis ~87 KB) — it is one translation unit by design. The other bridge files stay focused. project/leanclr/*.dllis gitignored; rebuilt DLLs land alongside but should not be committed.compatibility_minimum = "4.4"inleanclr.gdextension, projectconfig/features = "4.6". The currentGODOT_CPP_BRANCHis 4.5 — bump together (see "Two Source Patches" above).- CMake honors
GODOT_CPP_ROOT/GODOT_CPP_LIBRARYfor prebuilt godot-cpp; otherwise itFetchContents from GitHub. On Windows + MSVC the runtime is forced toMultiThreadedDLLto match whatgodot-cppships. - LeanCLR is a submodule: changes inside
thirdparty/leanclr/are not part of this repo's history. Don't edit it locally — patch upstream. - No automated test suite. The demo is the smoke test; run the Flappy scene and exercise hot reload to validate a change.
- The Godot binary on this machine is at
D:\Projects\godot-build\build\godot.exe(Godot 4.6.3). Pass it as a positional arg to--path projectto launch.