Files
2026-06-07 22:55:56 +08:00

120 lines
12 KiB
Markdown

# 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](https://github.com/focus-creative-games/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:
```bash
# 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 `ClassDB``ClassDBSingleton` 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:
```c
#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 `FetchContent`s 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.