From 7b42a6be63b95b567f83c0b60f3dc2a3c786d5b4 Mon Sep 17 00:00:00 2001 From: lingdar77 Date: Sun, 7 Jun 2026 22:55:56 +0800 Subject: [PATCH] build steps --- CLAUDE.md | 119 +++++++++++++++++++ CMakeLists.txt | 15 ++- README.md | 102 ++++++++++++++-- project/export_presets.cfg | 6 +- project/leanclr.gdextension | 1 + scripts/build-web.bat | 44 +++++++ scripts/build-web.sh | 71 +++++++++++ scripts/build-windows.bat | 30 +++++ scripts/build-windows.sh | 35 ++++++ scripts/clean.bat | 16 +++ scripts/clean.sh | 18 +++ src/leanclr_script.cpp | 5 +- thirdparty/leanclr | 2 +- tools/binding_generator/generate_bindings.py | 21 +++- 14 files changed, 467 insertions(+), 18 deletions(-) create mode 100644 CLAUDE.md create mode 100644 scripts/build-web.bat create mode 100644 scripts/build-web.sh create mode 100644 scripts/build-windows.bat create mode 100644 scripts/build-windows.sh create mode 100644 scripts/clean.bat create mode 100644 scripts/clean.sh diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3da5acc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,119 @@ +# 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//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 `true` and `true` (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 `` (which no longer exists) to `namespace godot::internal` inside ``. Fix in `src/leanclr_script.cpp`: + - Replaced `#include ` with `#include `. + - 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.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. diff --git a/CMakeLists.txt b/CMakeLists.txt index ea4261b..b5abead 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,14 @@ option(LEANCLR_GODOT_FETCH_GODOT_CPP "Fetch godot-cpp when GODOT_CPP_ROOT is not set(GODOT_CPP_ROOT "" CACHE PATH "Path to a checked-out godot-cpp tree") set(GODOT_CPP_LIBRARY "" CACHE FILEPATH "Path to a prebuilt godot-cpp static library. When set, godot-cpp is imported instead of built.") set(GODOT_CPP_GEN_INCLUDE_DIR "" CACHE PATH "Path to godot-cpp generated headers, for example /gen/include") -set(GODOT_CPP_BRANCH "4.4" CACHE STRING "godot-cpp branch or tag to use when fetching") +set(GODOT_CPP_BRANCH "4.5" CACHE STRING "godot-cpp branch or tag to use when fetching") + +# Only the Web build is single-threaded today — pthreads in a wasm side +# module is experimental and the demo does not need it. Forward to godot-cpp +# so its own -sUSE_PTHREADS=1 (see godot-cpp/cmake/web.cmake) is not emitted. +if(EMSCRIPTEN) + set(GODOTCPP_THREADS OFF CACHE BOOL "godot-cpp threading support" FORCE) +endif() if(MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" CACHE STRING "Select the MSVC runtime library for GDExtension binaries." FORCE) @@ -119,6 +126,12 @@ endif() if(EMSCRIPTEN) target_link_options(leanclr_godot PRIVATE -sSIDE_MODULE=1) set_target_properties(leanclr_godot PROPERTIES SUFFIX ".wasm") + # The auto-generated godot_api.generated.cpp has thousands of invoker + # functions; at -O3 the compiler inlines argument unpacking and blows + # past V8's 50,000-locals-per-function limit ("local count too large" at + # runtime). Cap the web build at -O1 to keep each function's locals + # manageable. + target_compile_options(leanclr_godot PRIVATE -O1) endif() set_target_properties(leanclr_godot PROPERTIES diff --git a/README.md b/README.md index ffc81c5..07a0013 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,23 @@ - 运行时热重载保留字段状态,接近 Python/Lua 那种 reload 手感 - demo 是一个简单 Flappy Bird,用来测输入、Process、状态迁移和重载 -先拉子模块: +## 编译 + +拉子模块: ```bash git submodule update --init --recursive ``` +LeanCLR 默认会把所有未实现的 icall 视为致命错误并 abort。本项目对接的 Godot API 只是一小部分,所以必须先把这个开关关掉,否则跑 demo 时第一碰到未实现接口就崩。修改 `thirdparty/leanclr/src/runtime/build_config.h`: + +```c +-#define LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1 ++#define LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 0 +``` + +> 这个 patch 在子模块里,`git submodule update` 之后需要重新打。 + 编 native 扩展: ```bash @@ -25,23 +36,101 @@ cmake -S . -B build-master cmake --build build-master --config Debug --target leanclr_godot ``` +> 第一次 `cmake -S . -B build-master` 会通过 FetchContent 拉 godot-cpp 4.5,但此时 `src/generated/godot_api.generated.cpp` 还不存在,配置会失败。所以顺序是: +> +> ```bash +> cmake -S . -B build-master +> python3 tools/binding_generator/generate_bindings.py --api build-master/_deps/godot-cpp-src/gdextension/extension_api.json +> cmake -S . -B build-master # 再配一次,让 add_library 看到生成的 cpp +> cmake --build build-master --config Debug --target leanclr_godot +> ``` + +> `GODOT_CPP_BRANCH` 默认是 `4.5`,仓库里已经为 4.5 打过两个补丁(见下方「已知补丁」)。 + 编 demo 的 C#: ```bash -dotnet msbuild project/Game.csproj /p:Configuration=Debug +dotnet msbuild managed/GodotSharpCompat/GodotSharpCompat.csproj -p:Configuration=Debug +dotnet msbuild project/Game.csproj -p:Configuration=Debug ``` -运行项目: +> `dotnet msbuild` 只能用单横线参数(`-p:...`),双横线(`/p:...`)会报 `MSB1008: 只能指定一个项目`。 +> Windows 上要用 `python3`,别用 `python`(会被 Microsoft Store 别名劫持弹出安装器)。 + +## 运行 ```bash -/Applications/Godot.app/Contents/MacOS/Godot --path project +"/path/to/Godot" --path project ``` -几个常看的文件: +> 把 `/path/to/Godot` 换成你机器上实际的 Godot 可执行文件路径。这台机器是 `D:/Projects/godot-build/build/godot.exe`,Godot 4.6.3。 + +## 已知补丁 + +### `thirdparty/leanclr` 子模块 + +- `src/runtime/build_config.h`:`LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR` 设为 `0`(见上文)。 + +### `src/leanclr_script.cpp` + +godot-cpp 4.5 把 `gdextension_interface_script_instance_create3` 从 `gdextension_interface::` 命名空间搬到了 `godot::internal::` 里,且原来的 `` 头文件已经不存在。要把: + +```cpp +#include +// ... +void* script_instance = gdextension_interface::script_instance_create3(&script_instance_info(), instance); +``` + +改成: + +```cpp +#include +// ... +void* script_instance = ::godot::internal::gdextension_interface_script_instance_create3(&script_instance_info(), instance); +``` + +### `tools/binding_generator/generate_bindings.py` + +godot-cpp 4.5 把 `ClassDB` 整体重命名成了 `ClassDBSingleton` 单例包装。生成器原本的 `CPP_HEADER_NAME_OVERRIDES["ClassDB"] = "class_db_singleton"` 只重命名了头文件路径,C++ 类名仍是 `ClassDB`,编译会爆 `get_class_static` 不存在。补丁:新增 `SINGLETON_CLASS_CPP_NAMES = {"ClassDB": "ClassDBSingleton"}`,让生成出的 C++ 走 `ClassDBSingleton::get_singleton()->method(...)`,C# 侧 `ClassDB` 的方法全部变成 `static`。 + +### `CMakeLists.txt`(Web 构建) + +仓库里已经为 Web 构建持久化地打了两个补丁,都在 `if(EMSCRIPTEN)` 块(或紧邻它的 `option` 块)里: + +1. **强制单线程**(Web 当前不支持多线程,且 `-sSIDE_MODULE + pthreads` 在 emcc 里是实验性的): + + ```cmake + if(EMSCRIPTEN) + set(GODOTCPP_THREADS OFF CACHE BOOL "godot-cpp threading support" FORCE) + endif() + ``` + +2. **降到 `-O1`**:自动生成的 `godot_api.generated.cpp` 里有几千个 invoker 函数,`-O3` 下编译器会把参数拆包全部内联,单个函数会塞进 11 万+ 局部变量,浏览器加载时会报 `Uncaught (in promise) CompileError: WebAssembly.instantiate(): Compiling function #N failed: local count too large`(V8 单函数局部变量上限 50,000)。`leanclr_godot` target 在 `if(EMSCRIPTEN)` 块里被显式加了 `target_compile_options(leanclr_godot PRIVATE -O1)`。 + +Web 构建方法: + +```bash +# 需要 Emscripten 3.1+、Ninja 都在 PATH 上 +rm -rf build-web +cmake -S . -B build-web -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build-web --target leanclr_godot +``` + +> 这台机器 Emscripten 在 `D:/Projects/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake`。 +> `leanclr.gdextension` 已经配了 `web.release.wasm32 = "res://bin/Release/leanclr_godot.wasm"`,构建产物会自动落到 `project/bin/Release/libleanclr_godot.wasm`。要跑起来用编辑器导出一份 Web 即可(或 `godot --headless --export-release "Web" output.html` 配 `project/export_presets.cfg`)。 +> +> 注意事项: +> - `-O1` 让 wasm 从 `-O3` 时的 26 MB 涨到 10 MB 左右(Release 去掉符号),是必要的代价。 +> - `leanclr_wasm_libcxx_shim.cpp` 提供 Emscripten libc++ 缺的 `std::__1::__hash_memory` 符号,Web 链接时会自动包含。 +> - `em++: warning: ignoring unsupported linker flag: -soname` 是无害的,`-soname` 是 ELF 链接器标志,emcc 不识别但 wasm 产物仍然正确。 + +## 几个常看的文件 `src/` : GDExtension 和 bridge。 -`managed/GodotSharpCompat/` : 给 C# 用的 Godot API 外观。 +`managed/GodotSharpCompat/` : 给 C# 用的 Godot API 外观。`Generated/` 子目录是绑定生成器的产物,`.gitignore` 已忽略。 `project/runtime_hot_reload_demo.tscn` : 现在的主 demo。 @@ -50,4 +139,3 @@ dotnet msbuild project/Game.csproj /p:Configuration=Debug `project/scripts/RuntimeCSharpEditor.gd` : 游戏里的 C# 编辑器窗口。 `project/leanclr/live_reload.txt` : 热重载 marker。写 `Game` 就回到默认 `Game.dll`,写别的 assembly 名就切到那个 dll。 - diff --git a/project/export_presets.cfg b/project/export_presets.cfg index e155df1..2fe255a 100644 --- a/project/export_presets.cfg +++ b/project/export_presets.cfg @@ -8,7 +8,7 @@ custom_features="" export_filter="all_resources" include_filter="leanclr/*.dll,leanclr/live_reload.txt" exclude_filter="" -export_path="deploy/html/index.html" +export_path="../../builds/leanCLR/web/index.html" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -24,8 +24,8 @@ script_export_mode=2 [preset.0.options] -custom_template/debug="/Volumes/External/Misc/templates_4.6/web_dlink_nothreads_debug.zip" -custom_template/release="/Volumes/External/Misc/templates_4.6/web_dlink_nothreads_release.zip" +custom_template/debug="" +custom_template/release="" variant/extensions_support=true variant/thread_support=false vram_texture_compression/for_desktop=true diff --git a/project/leanclr.gdextension b/project/leanclr.gdextension index 05e56f3..15efab2 100644 --- a/project/leanclr.gdextension +++ b/project/leanclr.gdextension @@ -13,3 +13,4 @@ linux.release.x86_64 = "res://bin/Release/libleanclr_godot.so" macos.debug = "res://bin/Debug/libleanclr_godot.dylib" macos.release = "res://bin/Release/libleanclr_godot.dylib" web.release.wasm32 = "res://bin/Release/libleanclr_godot.wasm" +web.debug.wasm32 = "res://bin/Release/libleanclr_godot.wasm" diff --git a/scripts/build-web.bat b/scripts/build-web.bat new file mode 100644 index 0000000..8d53315 --- /dev/null +++ b/scripts/build-web.bat @@ -0,0 +1,44 @@ +@echo off +REM Build the Web (WASM) target. Requires Emscripten 3.1+ and Ninja on PATH. + +setlocal + +if not exist thirdparty\leanclr\CMakeLists.txt ( + git submodule update --init --recursive +) + +findstr /C:"LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1" thirdparty\leanclr\src\runtime\build_config.h >nul 2>&1 +if %errorlevel% == 0 ( + echo [build-web] Re-applying leanclr NotImplemented patch... + powershell -Command "(Get-Content thirdparty/leanclr/src/runtime/build_config.h) -replace 'LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1', 'LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 0' | Set-Content thirdparty/leanclr/src/runtime/build_config.h" +) + +REM Find the Emscripten toolchain. Override via EMSDK_TOOLCHAIN if needed. +if "%EMSDK_TOOLCHAIN%"=="" ( + for /f "delims=" %%i in ('where emcc 2^>nul') do set "EMCC_PATH=%%i" + if not "%EMCC_PATH%"=="" ( + REM \upstream\emscripten\emcc -> \upstream\emscripten\cmake\Modules\Platform\Emscripten.cmake + for %%i in ("%EMCC_PATH%") do set "EMCC_DIR=%%~dpi" + set "EMSDK_TOOLCHAIN=%EMCC_DIR%cmake\Modules\Platform\Emscripten.cmake" + ) +) +if "%EMSDK_TOOLCHAIN%"=="" ( + echo [build-web] Emscripten toolchain not found. Set EMSDK_TOOLCHAIN to: 1>&2 + echo [build-web] ^\upstream\emscripten\cmake\Modules\Platform\Emscripten.cmake 1>&2 + exit /b 1 +) +echo [build-web] Using toolchain: %EMSDK_TOOLCHAIN% + +cmake -S . -B build-web -G Ninja -DCMAKE_TOOLCHAIN_FILE="%EMSDK_TOOLCHAIN%" -DCMAKE_BUILD_TYPE=Release || exit /b 1 +python tools\binding_generator\generate_bindings.py --api build-web\_deps\godot-cpp-src\gdextension\extension_api.json +if errorlevel 1 ( + py tools\binding_generator\generate_bindings.py --api build-web\_deps\godot-cpp-src\gdextension\extension_api.json +) +cmake -S . -B build-web -G Ninja -DCMAKE_TOOLCHAIN_FILE="%EMSDK_TOOLCHAIN%" -DCMAKE_BUILD_TYPE=Release || exit /b 1 +cmake --build build-web --target leanclr_godot || exit /b 1 + +dotnet msbuild managed\GodotSharpCompat\GodotSharpCompat.csproj -p:Configuration=Debug || exit /b 1 +dotnet msbuild project\Game.csproj -p:Configuration=Debug || exit /b 1 + +echo [build-web] Done. Output: project\bin\Release\libleanclr_godot.wasm +echo [build-web] Export the project for Web in the Godot editor to run. diff --git a/scripts/build-web.sh b/scripts/build-web.sh new file mode 100644 index 0000000..9548f7e --- /dev/null +++ b/scripts/build-web.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Build the Web (WASM) target. +# Requires Emscripten 3.1+ and Ninja on PATH. +# Run from the repo root. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +if [ ! -f "thirdparty/leanclr/CMakeLists.txt" ]; then + git submodule update --init --recursive +fi + +# Re-apply the LeanCLR NotImplemented patch if needed. +if grep -q "LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1" \ + thirdparty/leanclr/src/runtime/build_config.h 2>/dev/null; then + echo "[build-web] Re-applying leanclr NotImplemented patch..." + sed -i 's/LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1/LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 0/' \ + thirdparty/leanclr/src/runtime/build_config.h +fi + +# Find the Emscripten toolchain. Override via $EMSDK_TOOLCHAIN if your install +# is in a non-default location. +if [ -z "${EMSDK_TOOLCHAIN:-}" ]; then + if command -v emcc >/dev/null 2>&1; then + # `which emcc` may resolve to either /upstream/emscripten/emcc + # (POSIX) or /upstream/bin/emcc.bat (Windows launcher). Walk up + # until we find a directory that contains cmake/Modules/Platform/. + EMCC_PATH="$(command -v emcc)" + d="$(dirname "$EMCC_PATH")" + for _ in 1 2 3 4 5 6 7 8; do + candidate="$d/cmake/Modules/Platform/Emscripten.cmake" + if [ -f "$candidate" ]; then + EMSDK_TOOLCHAIN="$candidate" + break + fi + d="$(dirname "$d")" + done + fi +fi +if [ -z "${EMSDK_TOOLCHAIN:-}" ] || [ ! -f "$EMSDK_TOOLCHAIN" ]; then + echo "[build-web] Emscripten toolchain not found. Set EMSDK_TOOLCHAIN to the" >&2 + echo "[build-web] full path of Emscripten.cmake, e.g.:" >&2 + echo "[build-web] /upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" >&2 + exit 1 +fi +echo "[build-web] Using toolchain: $EMSDK_TOOLCHAIN" + +# Configure (first pass — fetches godot-cpp). Single-threaded is forced by +# the CMakeLists's own if(EMSCRIPTEN) block (GODOTCPP_THREADS=OFF, -O1). +cmake -S . -B build-web -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$EMSDK_TOOLCHAIN" \ + -DCMAKE_BUILD_TYPE=Release + +# Generate bindings (uses the same extension_api.json path, just inside build-web/). +python3 tools/binding_generator/generate_bindings.py \ + --api build-web/_deps/godot-cpp-src/gdextension/extension_api.json + +# Re-configure so the generated cpp is part of the target, then build. +cmake -S . -B build-web -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$EMSDK_TOOLCHAIN" \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build-web --target leanclr_godot + +# Managed C# (same output dir as the desktop build). +dotnet msbuild managed/GodotSharpCompat/GodotSharpCompat.csproj -p:Configuration=Debug +dotnet msbuild project/Game.csproj -p:Configuration=Debug + +echo "[build-web] Done. Output:" +echo "[build-web] project/bin/Release/libleanclr_godot.wasm" +echo "[build-web] Export the project for Web in the Godot editor to run." diff --git a/scripts/build-windows.bat b/scripts/build-windows.bat new file mode 100644 index 0000000..05fb6a0 --- /dev/null +++ b/scripts/build-windows.bat @@ -0,0 +1,30 @@ +@echo off +REM Build the Windows (Desktop) target: native GDExtension + managed C# DLLs. +REM Run from the repo root. + +setlocal + +if not exist thirdparty\leanclr\CMakeLists.txt ( + git submodule update --init --recursive +) + +REM Re-apply the LeanCLR NotImplemented patch if a fresh submodule overwrote it. +findstr /C:"LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1" thirdparty\leanclr\src\runtime\build_config.h >nul 2>&1 +if %errorlevel% == 0 ( + echo [build-windows] Re-applying leanclr NotImplemented patch... + powershell -Command "(Get-Content thirdparty/leanclr/src/runtime/build_config.h) -replace 'LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1', 'LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 0' | Set-Content thirdparty/leanclr/src/runtime/build_config.h" +) + +cmake -S . -B build-master -DCMAKE_BUILD_TYPE=Debug || exit /b 1 +python tools\..\..\tools\binding_generator\generate_bindings.py --api build-master\_deps\godot-cpp-src\gdextension\extension_api.json +if errorlevel 1 ( + REM python3 not on PATH; try py launcher as a fallback. + py tools\binding_generator\generate_bindings.py --api build-master\_deps\godot-cpp-src\gdextension\extension_api.json +) +cmake -S . -B build-master -DCMAKE_BUILD_TYPE=Debug || exit /b 1 +cmake --build build-master --config Debug --target leanclr_godot || exit /b 1 + +dotnet msbuild managed\GodotSharpCompat\GodotSharpCompat.csproj -p:Configuration=Debug || exit /b 1 +dotnet msbuild project\Game.csproj -p:Configuration=Debug || exit /b 1 + +echo [build-windows] Done. Run with: "C:\path\to\Godot.exe" --path project diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100644 index 0000000..6a19016 --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Build the Windows (Desktop) target: native GDExtension + managed C# DLLs. +# Run from the repo root. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +# Pull submodules if missing. +if [ ! -f "thirdparty/leanclr/CMakeLists.txt" ]; then + git submodule update --init --recursive +fi + +# Re-apply the LeanCLR NotImplemented patch if a fresh submodule overwrote it. +if grep -q "LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1" \ + thirdparty/leanclr/src/runtime/build_config.h 2>/dev/null; then + echo "[build-windows] Re-applying leanclr NotImplemented patch..." + sed -i 's/LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1/LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 0/' \ + thirdparty/leanclr/src/runtime/build_config.h +fi + +# Configure CMake (first pass — fetches godot-cpp via FetchContent). +cmake -S . -B build-master -DCMAKE_BUILD_TYPE=Debug +# Generate bindings (needs the just-fetched extension_api.json). +python3 tools/binding_generator/generate_bindings.py \ + --api build-master/_deps/godot-cpp-src/gdextension/extension_api.json +# Re-configure so add_library sees the generated cpp, then build. +cmake -S . -B build-master -DCMAKE_BUILD_TYPE=Debug +cmake --build build-master --config Debug --target leanclr_godot + +# Managed C#. +dotnet msbuild managed/GodotSharpCompat/GodotSharpCompat.csproj -p:Configuration=Debug +dotnet msbuild project/Game.csproj -p:Configuration=Debug + +echo "[build-windows] Done. Run with: \"/path/to/Godot\" --path project" diff --git a/scripts/clean.bat b/scripts/clean.bat new file mode 100644 index 0000000..d5b8c10 --- /dev/null +++ b/scripts/clean.bat @@ -0,0 +1,16 @@ +@echo off +REM Remove all build outputs. Leaves sources, scripts, and the leanclr +REM submodule intact. + +setlocal + +if exist build rmdir /s /q build +if exist build-master rmdir /s /q build-master +if exist build-web rmdir /s /q build-web +if exist src\generated rmdir /s /q src\generated +if exist managed\GodotSharpCompat\Generated rmdir /s /q managed\GodotSharpCompat\Generated +if exist project\bin rmdir /s /q project\bin +if exist project\leanclr rmdir /s /q project\leanclr +if exist project\obj rmdir /s /q project\obj + +echo [clean] Removed build directories, generated bindings, C# outputs. diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100644 index 0000000..ab9e878 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Remove all build outputs. Leaves sources, scripts, and the leanclr +# submodule intact. +# Run from the repo root. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +rm -rf build build-master build-web +rm -rf src/generated +rm -rf managed/GodotSharpCompat/Generated +rm -rf project/bin +rm -rf project/leanclr +rm -rf project/obj +rm -f tools/binding_generator/__pycache__ + +echo "[clean] Removed build directories, generated bindings, C# outputs." diff --git a/src/leanclr_script.cpp b/src/leanclr_script.cpp index 6632af2..ded2349 100644 --- a/src/leanclr_script.cpp +++ b/src/leanclr_script.cpp @@ -10,7 +10,8 @@ #include #include #include -#include +#include + #include #include #include @@ -491,7 +492,7 @@ void* LeanCLRScript::_instance_create(Object* p_for_object) const instance->managed_object = managed_object; LeanCLRRuntimeBridge::register_script_object(p_for_object, managed_object); - void* script_instance = gdextension_interface::script_instance_create3(&script_instance_info(), instance); + void* script_instance = ::godot::internal::gdextension_interface_script_instance_create3(&script_instance_info(), instance); if (script_instance == nullptr) { LeanCLRRuntimeBridge::unregister_script_object(p_for_object, managed_object); diff --git a/thirdparty/leanclr b/thirdparty/leanclr index dbe2516..f8f545b 160000 --- a/thirdparty/leanclr +++ b/thirdparty/leanclr @@ -1 +1 @@ -Subproject commit dbe2516ac6f5eeab7ec7e6c8a9d72014996a7588 +Subproject commit f8f545be20fd23d1cadda31e6065b9278ed3d7cb diff --git a/tools/binding_generator/generate_bindings.py b/tools/binding_generator/generate_bindings.py index 04dcdf3..9ed1db3 100644 --- a/tools/binding_generator/generate_bindings.py +++ b/tools/binding_generator/generate_bindings.py @@ -26,6 +26,10 @@ CPP_HEADER_NAME_OVERRIDES = { "Generic6DOFJoint3D": "generic6_dof_joint3d", "GradientTexture1D": "gradient_texture1_d", } +# godot-cpp 4.5 renamed ClassDB to a ClassDBSingleton wrapper. Map the API +# class name to the renamed C++ class and treat all of its methods as static +# (so the generated native side calls ClassDBSingleton::get_singleton()->...). +SINGLETON_CLASS_CPP_NAMES = {"ClassDB": "ClassDBSingleton"} HANDWRITTEN_PARTIAL_CLASSES = {"GodotObject", "Node"} CS_KEYWORDS = { "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", @@ -386,14 +390,14 @@ def build_generated_method(class_name, api_method, class_names, classes, global_ return GeneratedMethod( class_name=class_name, cs_class_name=cs_class_name(class_name), - cpp_class_name=class_name, + cpp_class_name=SINGLETON_CLASS_CPP_NAMES.get(class_name, class_name), api_name=api_name, cpp_name=cpp_name, cs_name=cs_name, icall_suffix=icall_suffix, return_type=return_type, args=tuple(args), - is_static=bool(api_method.get("is_static")), + is_static=bool(api_method.get("is_static")) or class_name in SINGLETON_CLASS_CPP_NAMES, is_virtual=is_virtual, is_vararg=is_vararg, ), None, None @@ -3311,7 +3315,10 @@ def generate_native_vararg_method_function(method): for arg in method.args: setup_lines.append(append_formal_vararg("args", arg)) setup_lines.append(" append_managed_varargs(args, p_varargs);") - target = method.cpp_class_name if method.is_static else "self" + if method.is_static and method.class_name in SINGLETON_CLASS_CPP_NAMES: + target = f"{method.cpp_class_name}::get_singleton()" + else: + target = method.cpp_class_name if method.is_static else "self" if method.return_type.category == "void": call_lines = [f" {target}->callv({call_method}, args);", *vararg_return_statement(method, "result").splitlines()] else: @@ -3331,7 +3338,13 @@ def generate_native_method_function(method): params = ([] if method.is_static else ["intptr_t p_native_ptr"]) + [cpp_native_param_declaration(arg) for arg in method.args] call_args = ", ".join(cpp_call_arg(arg) for arg in method.args) if method.is_static: - call = f"{method.cpp_class_name}::{method.cpp_name}({call_args})" if call_args else f"{method.cpp_class_name}::{method.cpp_name}()" + if method.class_name in SINGLETON_CLASS_CPP_NAMES: + # godot-cpp 4.5 exposes ClassDB methods as non-static on + # ClassDBSingleton; route them through the singleton instance. + target = f"{method.cpp_class_name}::get_singleton()" + call = f"{target}->{method.cpp_name}({call_args})" if call_args else f"{target}->{method.cpp_name}()" + else: + call = f"{method.cpp_class_name}::{method.cpp_name}({call_args})" if call_args else f"{method.cpp_class_name}::{method.cpp_name}()" return f"""{method.return_type.native_type} {fn}({', '.join(params)}) noexcept {{ {cpp_return_statement(method, call)}