Files
leanCLR_gdext/CLAUDE.md
T
2026-06-07 22:55:56 +08:00

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 via project/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 by project/Game.csproj and managed/GodotSharpCompat/GodotSharpCompat.csproj via HintPath. They are not built here; they ship inside thirdparty/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:

  1. ClassDB was renamed to a ClassDBSingleton wrapper in godot-cpp 4.5. The CPP_HEADER_NAME_OVERRIDES["ClassDB"] = "class_db_singleton" in tools/binding_generator/generate_bindings.py only renames the include path; the C++ class symbol in generated code is still ClassDB and breaks compilation. Fix: added a SINGLETON_CLASS_CPP_NAMES map in the generator that:

    • Maps ClassDBClassDBSingleton in the C++ class name emitted in the generated native function.
    • Forces is_static=True for all ClassDB methods (since 4.5 only exposes them as non-static on the singleton instance, accessed via ClassDBSingleton::get_singleton()).
    • Routes the singleton dispatch through ClassDBSingleton::get_singleton()->method(...) in both the regular and vararg generate_native_method_function paths.
    • The C# side correspondingly gets static methods on ClassDB (still extending GodotObject, so existing type-checks pass).
  2. gdextension_interface_script_instance_create3 moved in godot-cpp 4.5 from <godot_cpp/core/gdextension_interface_loader.hpp> (which no longer exists) to namespace godot::internal inside <godot_cpp/godot.hpp>. Fix in src/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(...).

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-written GD, Node, GodotObject, ExportAttribute, Variant/marshalling helpers, CallableDelegateRegistry; plus a Generated/ 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 both src/generated/godot_api.generated.cpp and managed/GodotSharpCompat/Generated/*.generated.cs
  • thirdparty/leanclr/ — LeanCLR git submodule, pulled via add_subdirectory(${LEANCLR_ROOT}/src/runtime leanclr_runtime) in the top-level CMake
  • gdextension_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 global LeanCLRVirtualMachine, loads assemblies by name from the configured assembly directory, creates/release managed objects, and exposes overloaded invoke_script_method / get_script_property / set_script_property for the typed value marshallers LeanCLR needs. Also handles owner→managed-object registration and migrate_script_state for hot reload.
  • LeanCLRScriptLanguage (leanclr_script_language.{h,cpp}) — ScriptLanguageExtension singleton. 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_scripts are what the runtime editor hits.
  • LeanCLRScript (leanclr_script.{h,cpp}) — ScriptExtension for an individual .cs / .lcs file. parse_source() extracts assembly_name, type_name, optional entry_point, and base_type (defaults to "Node") from header comments via a simple parser in this file. Calls godot::internal::gdextension_interface_script_instance_create3 to allocate script instances.
  • LeanCLRScriptLoader / LeanCLRScriptSaver (leanclr_script_{loader,saver}.{h,cpp}) — ResourceFormatLoader/Saver pair. Recognizes cs and lcs extensions, hands a LeanCLRScript resource to Godot so .cs files can be attached to nodes and saved in .tscn/.tres.
  • LeanCLRMain (leanclr_main_node.{h,cpp}) — a Node that, on NOTIFICATION_READY, instantiates the configured C# class (default Game.ClassDbMain) through the bridge. Used by main.tscn as the bootstrapping demo node.
  • LeanCLRHotReloadHost (leanclr_hot_reload_host.{h,cpp}) — a Node that owns the current managed object for runtime hot reload. Wires _Input/process through the bridge, and on reload_assembly captures state via CaptureHotReloadState (custom Variant blob from the script), instantiates the new assembly, calls migrate_script_state, then RestoreHotReloadState and OnHotReloaded on the new instance. Skips itself while in the editor or while a scene is being edited.
  • leanclr_wasm_libcxx_shim.cpp — minimal std::__1::__hash_memory shim 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:

  1. FlappyScript (a LeanCLRScript pointing at project/scripts/HotReloadSmoke.cs) is the gameplay node.
  2. LiveHotReloadHost (a LeanCLRHotReloadHost) is the bridge that drives the C# object's lifecycle.
  3. HotReloadInputRelay.gd forwards input and polls res://leanclr/live_reload.txt every reload_poll_seconds. When the marker changes from the default Game, it calls reload_assembly on the host.
  4. RuntimeCSharpEditor.gd is a CodeEdit window inside the game. Pressing Run writes HotReloadSmoke.cs back, shells out to dotnet msbuild to produce a uniquely-named GameRuntimeEdit<unix_ms>.dll into project/leanclr/, then writes that name to live_reload.txt so the relay picks it up on its next tick. It also rebuilds the default Game.dll afterward and restores the runtime-edit dll from user:// so the next editor run is reproducible.
  5. HotReloadSmoke.cs implements CaptureHotReloadState / RestoreHotReloadState / OnHotReloaded so the bird/physics state survives across reloads. Setting the env var LEANCLR_RUNTIME_EDITOR_AUTORUN triggers a one-shot edit-and-rebuild on startup to verify the path.

Conventions & Gotchas

  • The C# GodotSharpCompat shim has a Generated/ subdir in its .gitignore; if you regenerate bindings keep it out of the repo.
  • The native bridge is large (leanclr_runtime_bridge.cpp is ~87 KB) — it is one translation unit by design. The other bridge files stay focused.
  • project/leanclr/*.dll is gitignored; rebuilt DLLs land alongside but should not be committed.
  • compatibility_minimum = "4.4" in leanclr.gdextension, project config/features = "4.6". The current GODOT_CPP_BRANCH is 4.5 — bump together (see "Two Source Patches" above).
  • CMake honors GODOT_CPP_ROOT / GODOT_CPP_LIBRARY for prebuilt godot-cpp; otherwise it FetchContents from GitHub. On Windows + MSVC the runtime is forced to MultiThreadedDLL to match what godot-cpp ships.
  • 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 project to launch.