From e80433906ad6afe6e91432f5c4c7abe5a0070a75 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 4 Aug 2026 05:18:34 +0800 Subject: [PATCH] feat: add compat.godot-cpp 10.0.0-rc1 (Godot 4.6) + TYPED_METHOD_BIND Upstream godot-cpp moved off "tag tracks the engine" (godot-4.5-stable) onto its own version line, and 10.0.0-rc1 is the first of it. Which engine it binds is in the api header rather than the tag: "version_full_name": "Godot Engine v4.6.stable.official" so the index now carries 4.5.0 (Godot 4.5) and 10.0.0-rc1 (Godot 4.6), each with its own workspace member. repack.sh dispatches on the actual signature and the actual files rather than on the tag: generate_bindings() grew an `interface_filepath` parameter, and gdextension_interface.h stopped being checked in -- it is generated from gdextension_interface.json into gen/include/. Re-running the updated script on godot-4.5-stable still reproduces b0c36e77..., so the change is backward compatible; 10.0.0-rc1 hashes identically across two runs. The descriptor takes the union of both layouts, catch2-style (a glob matching nothing is skipped): 10.x adds one .cpp directly under gen/src/. TYPED_METHOD_BIND is the second half of this change, and it is a bug fix. Without it, method_bind.hpp reinterpret_casts member pointers through a FORWARD-DECLARED `_gde_UnexistingClass`; under the MSVC ABI a pointer-to-member's size depends on the class's inheritance model, so for an incomplete class the cast is rejected: error: cannot reinterpret_cast from member pointer type 'double (TestSprite::*)() const' to member pointer type 'double (_gde_UnexistingClass::*)() const' of different size i.e. EVERY ClassDB::bind_method call failed to compile on Windows. Upstream's cmake sets it PUBLIC on MSVC for exactly this reason. It rides on the default feature unconditionally rather than per-OS: it is a header switch that changes MethodBindT's template parameter list, so library and consumer must agree, and the cost off MSVC is only extra template instantiation. WINDOWS_ENABLED and NOMINMAX, which upstream sets alongside, are not needed -- neither appears anywhere in the shipped headers or sources. Both members now build a GDCLASS subclass with bound methods, which is what the Windows leg was missing: the old assertions were pure math and never reached bind_method, so a guaranteed compile error went unseen. godot-cpp-v10 additionally asserts GODOT_VERSION_MAJOR/MINOR == 4/6 and the presence of EditorDock (4.6-only), so the two members cannot be confused for each other. Verified locally with the CI-pinned mcpp 2026.8.3.3: godot-cpp -> bind=1 vec2=1 vec3=1 basis=1 color=1 aabb=1 gen=1 ... ok godot-cpp-v10 -> version=1 bind=1 vec2=1 ... gen=1 ... ok --- .../2026-08-04-add-godot-cpp-10.0.0-rc1.md | 93 ++++++++++++++++ README.md | 2 +- README.zh-CN.md | 2 +- mcpp.toml | 1 + pkgs/c/compat.godot-cpp.lua | 52 ++++++++- tests/examples/godot-cpp-v10/mcpp.toml | 15 +++ .../godot-cpp-v10/tests/godot_cpp_v10.cpp | 104 ++++++++++++++++++ tests/examples/godot-cpp/tests/godot_cpp.cpp | 41 ++++++- tools/godot-cpp/repack.sh | 21 +++- 9 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 .agents/docs/2026-08-04-add-godot-cpp-10.0.0-rc1.md create mode 100644 tests/examples/godot-cpp-v10/mcpp.toml create mode 100644 tests/examples/godot-cpp-v10/tests/godot_cpp_v10.cpp diff --git a/.agents/docs/2026-08-04-add-godot-cpp-10.0.0-rc1.md b/.agents/docs/2026-08-04-add-godot-cpp-10.0.0-rc1.md new file mode 100644 index 0000000..b398ef0 --- /dev/null +++ b/.agents/docs/2026-08-04-add-godot-cpp-10.0.0-rc1.md @@ -0,0 +1,93 @@ +# compat.godot-cpp 增加 10.0.0-rc1(Godot 4.6),并补上 MSVC ABI 缺的 define + +日期:2026-08-04 · 承接 [2026-08-04-add-godot-cpp-plan.md](2026-08-04-add-godot-cpp-plan.md)(4.5.0,PR #143) + +## 1. 版本线:godot-cpp 有了自己的版本号 + +上游 godot-cpp 的 tag 从「跟 Godot 走」(`godot-4.5-stable`)换成了**自己的版本线**: +[`10.0.0-rc1`](https://github.com/godotengine/godot-cpp/releases/tag/10.0.0-rc1)。它绑定的引擎版本写在 +`gdextension/extension_api.json` 的 header 里: + +```json +{ "version_major": 4, "version_minor": 6, "version_patch": 0, "version_status": "stable", + "version_full_name": "Godot Engine v4.6.stable.official" } +``` + +即 **10.0.0-rc1 = Godot 4.6**。索引里两条版本并存,消费者按需选: + +| 索引版本 | 上游 tag | 引擎 | +|---|---|---| +| `4.5.0` | `godot-4.5-stable` | Godot 4.5 | +| `10.0.0-rc1` | `10.0.0-rc1` | Godot 4.6 | + +`mcpp xpkg parse` 接受带预发布后缀的 `10.0.0-rc1`,lint 也只拦前导 `v`,无需特殊处理。 + +## 2. repack 脚本要兼容两代 API + +10.x 改了两处,`tools/godot-cpp/repack.sh` 按**实际签名/实际文件**判定而不是按 tag 判定: + +- `generate_bindings()` 多了 `interface_filepath` 参数(用 `inspect.signature` 探测); +- `gdextension_interface.h` **不再签入**,改为由 `gdextension/gdextension_interface.json` 生成到 + `gen/include/`(存在哪个就传哪个,与 cmake 的 `GODOTCPP_GDEXTENSION_INTERFACE_FILE` 同逻辑)。 + +改完后重跑 4.5.0 仍得到同一个 sha `b0c36e77…`,即向后兼容;10.0.0-rc1 连跑两次得 +`aaafbf50d4b8469d610fdb2eb76c6f58d758dbabbc6b013f60464d99b20ceb6e`。 + +## 3. 描述符:两种布局取并集 + +10.x 的 `gen/src/` 多了一个直接位于其下的 `.cpp`(4.5 只有 `classes/`、`variant/` 两层),故 sources +加一条 `*/gen/src/*.cpp`。**匹配不到的 glob 会被跳过**,这是 compat.catch2 已经在用的做法(v2 走 +`single_include`、v3 走 `src`,另一个空着)。`include_dirs` 同时保留 `*/gdextension` 与 `*/gen/include`, +因为那个 C ABI 头在两代里位置不同。 + +## 4. TYPED_METHOD_BIND —— MSVC ABI 上不是可选项 + +`godot-cpp-m` 的 Windows CI 暴露出来的:任何 `ClassDB::bind_method` 调用在 clang-cl 下直接编译失败 + +``` +error: cannot reinterpret_cast from member pointer type 'double (TestSprite::*)() const' + to member pointer type 'double (_gde_UnexistingClass::*)() const' of different size +``` + +`method_bind.hpp` 在 `#ifndef TYPED_METHOD_BIND` 时把成员指针 cast 成一个**前向声明**的 +`_gde_UnexistingClass`;MSVC ABI 下成员指针的大小取决于该类的继承模型,不完整类型只能按最一般形式 +假定,于是尺寸对不上、cast 非法。上游 `cmake/windows.cmake` 正是为此在 MSVC 下把 +`TYPED_METHOD_BIND` 设为 **PUBLIC**。 + +本包把它挂在默认 feature 上(与 `GDEXTENSION` 同处),**不按平台分**:它是个改 +`MethodBindT` 模板参数表的**头文件开关**,库与消费者必须一致,统一一个答案比按 OS 分更容易保证。 +非 MSVC 侧的代价只是多一些模板实例化,无行为差异。上游一起设的 `WINDOWS_ENABLED` / `NOMINMAX` +**不需要** —— 在 4.5 与 10.x 的头文件和源码里都一次都没出现过。 + +## 5. 测试补了 GDCLASS + bind_method + +两个成员(`godot-cpp`、`godot-cpp-v10`)都加了一个 `GDCLASS` 子类,带两个 `ClassDB::bind_method` +绑定和一次对 GDCLASS 生成物的 ODR-use。**这正是之前 Windows 绿得没有意义的原因**:老测试只碰纯数学, +根本没走到 bind_method,所以上面那个必现的编译错误一次都没被 CI 看见。断言是「编得过且链得上」—— +不能真调用,ClassDB/StringName 都要走 `gdextension_interface_*` 函数指针。 + +`godot-cpp-v10` 另外断言 `GODOT_VERSION_MAJOR/MINOR == 4/6` 且 4.6 才有的 `EditorDock` 存在, +用来证明拿到的确实是 10.x 那套绑定而不是 4.5 的。 + +## 6. 本地验证 + +``` +$ mcpp test -p godot-cpp # 4.5.0 +bind=1 vec2=1 vec3=1 basis=1 color=1 aabb=1 gen=1 + test result ok. 1 passed; 0 failed; finished in 59.15s + +$ mcpp test -p godot-cpp-v10 # 10.0.0-rc1 +version=1 bind=1 vec2=1 vec3=1 basis=1 color=1 aabb=1 gen=1 + test result ok. 1 passed; 0 failed; finished in 52.72s +``` + +另外 10.0.0-rc1 的 1075 个 TU 用 gcc 13 `-std=c++23 -fPIC` 全量编过,零失败。 + +## 7. 镜像 + +| 区域 | 地址 | +|---|---| +| GLOBAL | `https://github.com/xlings-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz` | +| CN | `https://gitcode.com/mcpp-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz` | + +两侧下载回来核过 sha,与本地打包一致。 diff --git a/README.md b/README.md index ed08b10..db0f245 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Two kinds of packages live here: | header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | Runtime loader compat (pure sources, sidestepping upstream codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua) (the Khronos loader: `loader/generated/` is checked in, and the assembly path degrades to plain C through `UNKNOWN_FUNCTIONS_SUPPORTED`, so no CMake/Python/assembler is needed; windows deferred) · [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) | | Whole-source direct build + generated config (only where a platform lacks one) | [`compat.curl`](pkgs/c/compat.curl.lua) (win32 uses upstream's checked-in config, unix generates one) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua) (win/mac use upstream's checked-in config; linux generates one and enables X11 by hand) | -| Upstream codegen frozen into the mirror archive | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua) (the ~1000 GDExtension classes under `gen/` exist in no upstream tag archive — upstream's `binding_generator.py` emits them at build time. Running it once offline and publishing upstream's tree byte-for-byte **plus** `gen/` keeps Python off the consumer side entirely; `tools/godot-cpp/repack.sh` reproduces the archive deterministically and refuses to publish if any upstream file differs) | +| Upstream codegen frozen into the mirror archive | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua) (two versions: `4.5.0` = the `godot-4.5-stable` bindings, `10.0.0-rc1` = godot-cpp's own 10.x line, whose bindings target Godot 4.6. The ~1000 GDExtension classes under `gen/` exist in no upstream tag archive — upstream's `binding_generator.py` emits them at build time. Running it once offline and publishing upstream's tree byte-for-byte **plus** `gen/` keeps Python off the consumer side entirely; `tools/godot-cpp/repack.sh` reproduces the archive deterministically and refuses to publish if any upstream file differs) | | Header package filling a gap in the index | [`compat.glx-headers`](pkgs/c/compat.glx-headers.lua) (libglvnd's `GL/glx.h`, absent from the Khronos registry and required by SDL's X11 backend) | | C++ application framework compat (dependencies reuse packages already in the index) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua) (upstream's `3rd/` ships 8 vendored dependencies; none of them is compiled here — all are redirected to the same-version `compat.*` packages in this index) | | Mutually exclusive backends (one of several inside one package) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua): `vulkan` / `sdl2` each **replace** the default OpenGL / GLFW, and the default backend is expressed by *naming no feature at all* — there is no `opengl`/`glfw` feature. A `default` feature cannot express exclusivity: its own `defines`/`sources`/`deps` have no effect whatsoever, while its `implies` always applies and cannot be overridden by a named feature (which is, conversely, exactly the solution for the "always-on interface define" row below). The workable answer is to read the `-DMCPP_FEATURE_` mcpp passes anyway and decide up front in a force-included header. Note also that `cflags` only reaches C TUs — C++ needs `cxxflags`, so a backend define written only into `cflags` never reaches any `.cpp` | diff --git a/README.zh-CN.md b/README.zh-CN.md index 4791a14..907e957 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -39,7 +39,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 | header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | 运行时 loader compat(纯源码,绕开上游 codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua)(Khronos loader:`loader/generated/` 已签入,汇编路径经 `UNKNOWN_FUNCTIONS_SUPPORTED` 降级为纯 C,故无需 CMake/Python/汇编器;windows 延后)· [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) | | 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) | -| 上游 codegen 前置冻结进镜像归档 | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua)(`gen/` 下约 1000 个 GDExtension 类不在任何上游 tag 归档里,由上游 `binding_generator.py` 在构建时生成。改为离线跑一次,把上游源码树逐字节原样 **加上** `gen/` 一起发布,消费侧就完全不需要 Python;`tools/godot-cpp/repack.sh` 可确定性复现该归档,且上游文件一旦有出入即拒绝打包) | +| 上游 codegen 前置冻结进镜像归档 | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua)(两个版本:`4.5.0` 是 `godot-4.5-stable` 的绑定,`10.0.0-rc1` 是 godot-cpp 自己的 10.x 线、对应 Godot 4.6。`gen/` 下约 1000 个 GDExtension 类不在任何上游 tag 归档里,由上游 `binding_generator.py` 在构建时生成。改为离线跑一次,把上游源码树逐字节原样 **加上** `gen/` 一起发布,消费侧就完全不需要 Python;`tools/godot-cpp/repack.sh` 可确定性复现该归档,且上游文件一旦有出入即拒绝打包) | | 补索引空缺的头文件包 | [`compat.glx-headers`](pkgs/c/compat.glx-headers.lua)(libglvnd 的 `GL/glx.h`,Khronos registry 不含,SDL 的 X11 后端必需) | | C++ 应用框架 compat(依赖复用索引内既有包) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua)(上游 `3rd/` 自带 8 个 vendored 依赖,此处一个不编,全部改指索引内同版本 `compat.*`) | | 互斥后端(同包多后端二选一) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua):`vulkan` / `sdl2` 各自**替换**默认的 OpenGL / GLFW,默认后端由"不点名任何 feature"表达,并不存在 `opengl`/`glfw` feature。`default` feature 表达不了互斥 —— 它自带的 `defines`/`sources`/`deps` 完全不生效,而 `implies` 又恒生效、无法被点名的 feature 覆盖(后者反而正好是本表『恒开的 interface define』一行的解法)。可行解是读 mcpp 本就会传的 `-DMCPP_FEATURE_`,在强制包含头里做前置判定。另注意 `cflags` 只作用于 C TU,C++ 需 `cxxflags` —— 只写进 `cflags` 的后端 define 到不了任何 `.cpp` | diff --git a/mcpp.toml b/mcpp.toml index 102c827..e67aabf 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -30,6 +30,7 @@ members = [ "tests/examples/ffmpeg-module", "tests/examples/fmtlib.fmt", "tests/examples/godot-cpp", + "tests/examples/godot-cpp-v10", "tests/examples/gui-stack", "tests/examples/imgui", "tests/examples/imgui-module", diff --git a/pkgs/c/compat.godot-cpp.lua b/pkgs/c/compat.godot-cpp.lua index 187f7b9..2a3bdd6 100644 --- a/pkgs/c/compat.godot-cpp.lua +++ b/pkgs/c/compat.godot-cpp.lua @@ -29,7 +29,25 @@ -- Defines -- GDEXTENSION is upstream's PUBLIC compile definition (cmake sets it on the -- godot-cpp target's INTERFACE), so it rides on a default feature: the lib --- and every consumer TU must agree. The layout-affecting ones are left +-- and every consumer TU must agree. +-- +-- TYPED_METHOD_BIND rides along, and is not optional on the MSVC ABI. +-- Without it, method_bind.hpp reinterpret_casts member pointers through a +-- FORWARD-DECLARED `_gde_UnexistingClass`; under the MSVC ABI a +-- pointer-to-member's size depends on the class's inheritance model, so for +-- an incomplete class clang-cl rejects the cast outright ("cannot +-- reinterpret_cast ... to member pointer type of different size") and every +-- ClassDB::bind_method call fails to compile. Upstream's cmake sets it +-- PUBLIC for exactly this reason ($<${IS_MSVC}: TYPED_METHOD_BIND ...> in +-- cmake/windows.cmake). It is set unconditionally rather than per-OS +-- because it is a HEADER switch that changes MethodBindT's template +-- parameter list -- library and consumer must agree on it, and one uniform +-- answer is cheaper to guarantee than an OS-conditional one. The cost off +-- MSVC is some extra template instantiation, which is why upstream keeps +-- the untyped path as its default there; there is no behavioural +-- difference. (WINDOWS_ENABLED and NOMINMAX, which upstream sets alongside, +-- are NOT needed: neither appears anywhere in the shipped headers or +-- sources.) The layout-affecting ones are left -- undefined on both sides, which is upstream's release default: -- DEBUG_ENABLED / DEV_ENABLED (extra checks), HOT_RELOAD_ENABLED (changes -- the Wrapped layout) and REAL_T_IS_DOUBLE (needs the double-precision @@ -54,6 +72,13 @@ package = { -- gdextension_interface.h ABI). xpm = { linux = { + ["10.0.0-rc1"] = { + url = { + GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz", + CN = "https://gitcode.com/mcpp-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz", + }, + sha256 = "aaafbf50d4b8469d610fdb2eb76c6f58d758dbabbc6b013f60464d99b20ceb6e", + }, ["4.5.0"] = { url = { GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz", @@ -63,6 +88,13 @@ package = { }, }, macosx = { + ["10.0.0-rc1"] = { + url = { + GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz", + CN = "https://gitcode.com/mcpp-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz", + }, + sha256 = "aaafbf50d4b8469d610fdb2eb76c6f58d758dbabbc6b013f60464d99b20ceb6e", + }, ["4.5.0"] = { url = { GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz", @@ -72,6 +104,13 @@ package = { }, }, windows = { + ["10.0.0-rc1"] = { + url = { + GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz", + CN = "https://gitcode.com/mcpp-res/godot-cpp/releases/download/10.0.0-rc1/godot-cpp-10.0.0-rc1.tar.gz", + }, + sha256 = "aaafbf50d4b8469d610fdb2eb76c6f58d758dbabbc6b013f60464d99b20ceb6e", + }, ["4.5.0"] = { url = { GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz", @@ -88,20 +127,27 @@ package = { import_std = false, -- Three roots, exactly as upstream's build systems expose them: -- hand-written headers, generated headers, and the GDExtension C ABI - -- header (gdextension_interface.h) that both of them include. + -- header that both of them include. Where that header lives moved + -- between the two versions -- 4.5 checks in gdextension/ + -- gdextension_interface.h, 10.x generates it into gen/include/ from + -- gdextension_interface.json -- so both roots stay listed. include_dirs = { "*/include", "*/gen/include", "*/gdextension" }, -- Enumerated rather than `**`: upstream's own test project ships a -- test/src/*.cpp that must not be swept into the library, and the two -- source roots are only ever one and two levels deep. + -- Union of both layouts, catch2-style: a glob that matches nothing on + -- a given version is simply skipped. 10.x adds one .cpp directly under + -- gen/src/ that 4.5 does not have. sources = { "*/src/*.cpp", "*/src/*/*.cpp", + "*/gen/src/*.cpp", "*/gen/src/*/*.cpp", }, targets = { ["godot-cpp"] = { kind = "lib" } }, features = { ["default"] = { implies = { "gdextension" } }, - ["gdextension"] = { defines = { "GDEXTENSION" } }, + ["gdextension"] = { defines = { "GDEXTENSION", "TYPED_METHOD_BIND" } }, }, deps = { }, -- A GDExtension IS a shared library, so this static library's objects diff --git a/tests/examples/godot-cpp-v10/mcpp.toml b/tests/examples/godot-cpp-v10/mcpp.toml new file mode 100644 index 0000000..12c2e9f --- /dev/null +++ b/tests/examples/godot-cpp-v10/mcpp.toml @@ -0,0 +1,15 @@ +# compat.godot-cpp 10.0.0-rc1 (Godot 4.6) test project. +# +# Separate member rather than a second dependency in the 4.5 one: the two +# versions are different bindings of different engine releases, and each has to +# be built and asserted on its own -- same shape as catch2 / catch2-v2. +# +# `compat` is redirected to this checkout by the workspace-root [indices], +# which every member inherits. + +[package] +name = "godot-cpp-v10-tests" +version = "0.1.0" + +[dependencies.compat] +godot-cpp = "10.0.0-rc1" diff --git a/tests/examples/godot-cpp-v10/tests/godot_cpp_v10.cpp b/tests/examples/godot-cpp-v10/tests/godot_cpp_v10.cpp new file mode 100644 index 0000000..3b7be67 --- /dev/null +++ b/tests/examples/godot-cpp-v10/tests/godot_cpp_v10.cpp @@ -0,0 +1,104 @@ +// Behavioral test for compat.godot-cpp 10.0.0-rc1 -- godot-cpp's own 10.x +// version line, whose bindings target Godot 4.6. +// +// The point of a second member is that it can tell the two versions apart, so +// the first thing asserted is WHICH bindings arrived: the generated version +// header and a class that exists in 4.6 and not in 4.5. The rest mirrors the +// 4.5 member so a behavioural difference between the two would show up as a +// diff between two otherwise identical tests. +// +// As there, everything asserted is pure math or compile-time: anything routed +// through the gdextension_interface_* pointers needs a Godot process that has +// loaded the extension. + +#include // new in Godot 4.6 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +bool close(double a, double b) { + return std::fabs(a - b) < 1e-5; +} + +} // namespace + +using namespace godot; + +// A GDCLASS subclass with bound methods -- the shape every GDExtension is +// written in, and the one that breaks first on a new platform: without +// TYPED_METHOD_BIND, ClassDB::bind_method casts member pointers through a +// forward-declared class, which the MSVC ABI rejects outright. Compiling and +// linking this is the assertion; it is never CALLED, because ClassDB and +// StringName go through the gdextension_interface_* pointers, which are null +// outside a Godot process that has loaded the extension. +class TestSprite : public Node { + GDCLASS(TestSprite, Node) + +protected: + static void _bind_methods() { + ClassDB::bind_method(D_METHOD("get_speed"), &TestSprite::get_speed); + ClassDB::bind_method(D_METHOD("set_speed", "speed"), &TestSprite::set_speed); + } + +public: + double get_speed() const { return speed; } + void set_speed(double p_speed) { speed = p_speed; } + +private: + double speed = 1.0; +}; + +int main() { + // these bindings are Godot 4.6, not the 4.5 the sibling member pins + const bool version_ok = GODOT_VERSION_MAJOR == 4 && + GODOT_VERSION_MINOR == 6 && + sizeof(EditorDock) > 0; + + // out-of-line variant math: proves the library linked + const bool vec2_ok = close(Vector2(3, 4).length(), 5.0) && + close(Vector2(3, 4).normalized().length(), 1.0); + + const Vector3 cross = Vector3(1, 0, 0).cross(Vector3(0, 1, 0)); + const bool vec3_ok = cross == Vector3(0, 0, 1) && + close(Vector3(2, 3, 6).length(), 7.0); + + const bool basis_ok = close(Basis().orthonormalized().determinant(), 1.0); + + const bool color_ok = Color(1.0f, 0.0f, 0.0f, 1.0f).to_rgba32() == 0xff0000ffu; + + const AABB box(Vector3(0, 0, 0), Vector3(2, 3, 4)); + const AABB other(Vector3(1, 1, 1), Vector3(4, 4, 4)); + const bool aabb_ok = close(box.get_volume(), 24.0) && + box.intersects(other) && + close(box.intersection(other).get_volume(), 6.0); + + const bool gen_ok = sizeof(Node) > 0 && + Node::PROCESS_MODE_INHERIT == 0 && + Node::PROCESS_MODE_DISABLED == 4 && + godot::OK == 0 && + godot::ERR_FILE_NOT_FOUND == 7 && + Variant::OBJECT != Variant::NIL; + + // ODR-use what GDCLASS generated without calling into the engine + const StringName &(*class_name_fn)() = &TestSprite::get_class_static; + void (*init_fn)() = &TestSprite::initialize_class; + const bool bind_ok = class_name_fn != nullptr && init_fn != nullptr && + std::is_base_of::value; + + const bool ok = bind_ok && version_ok && vec2_ok && vec3_ok && basis_ok && color_ok && + aabb_ok && gen_ok; + std::printf("version=%d bind=%d vec2=%d vec3=%d basis=%d color=%d aabb=%d gen=%d\n", + version_ok, bind_ok, vec2_ok, vec3_ok, basis_ok, color_ok, aabb_ok, gen_ok); + return ok ? 0 : 1; +} diff --git a/tests/examples/godot-cpp/tests/godot_cpp.cpp b/tests/examples/godot-cpp/tests/godot_cpp.cpp index afab726..d59b2e9 100644 --- a/tests/examples/godot-cpp/tests/godot_cpp.cpp +++ b/tests/examples/godot-cpp/tests/godot_cpp.cpp @@ -25,6 +25,7 @@ #include #include +#include #include namespace { @@ -35,9 +36,33 @@ bool close(double a, double b) { } // namespace -int main() { - using namespace godot; +using namespace godot; + +// A GDCLASS subclass with bound methods -- the shape every GDExtension is +// written in, and the one that breaks first on a new platform: without +// TYPED_METHOD_BIND, ClassDB::bind_method casts member pointers through a +// forward-declared class, which the MSVC ABI rejects outright. Compiling and +// linking this is the assertion; it is never CALLED, because ClassDB and +// StringName go through the gdextension_interface_* pointers, which are null +// outside a Godot process that has loaded the extension. +class TestSprite : public Node { + GDCLASS(TestSprite, Node) + +protected: + static void _bind_methods() { + ClassDB::bind_method(D_METHOD("get_speed"), &TestSprite::get_speed); + ClassDB::bind_method(D_METHOD("set_speed", "speed"), &TestSprite::set_speed); + } + +public: + double get_speed() const { return speed; } + void set_speed(double p_speed) { speed = p_speed; } +private: + double speed = 1.0; +}; + +int main() { // --- out-of-line variant math: proves the library linked --- const bool vec2_ok = close(Vector2(3, 4).length(), 5.0) && close(Vector2(3, 4).normalized().length(), 1.0); @@ -66,8 +91,14 @@ int main() { godot::SIDE_LEFT == 0 && Variant::OBJECT != Variant::NIL; - const bool ok = vec2_ok && vec3_ok && basis_ok && color_ok && aabb_ok && gen_ok; - std::printf("vec2=%d vec3=%d basis=%d color=%d aabb=%d gen=%d\n", - vec2_ok, vec3_ok, basis_ok, color_ok, aabb_ok, gen_ok); + // ODR-use what GDCLASS generated without calling into the engine + const StringName &(*class_name_fn)() = &TestSprite::get_class_static; + void (*init_fn)() = &TestSprite::initialize_class; + const bool bind_ok = class_name_fn != nullptr && init_fn != nullptr && + std::is_base_of::value; + + const bool ok = bind_ok && vec2_ok && vec3_ok && basis_ok && color_ok && aabb_ok && gen_ok; + std::printf("bind=%d vec2=%d vec3=%d basis=%d color=%d aabb=%d gen=%d\n", + bind_ok, vec2_ok, vec3_ok, basis_ok, color_ok, aabb_ok, gen_ok); return ok ? 0 : 1; } diff --git a/tools/godot-cpp/repack.sh b/tools/godot-cpp/repack.sh index efd0382..190f4c0 100755 --- a/tools/godot-cpp/repack.sh +++ b/tools/godot-cpp/repack.sh @@ -43,9 +43,28 @@ test -d "$WORK/$WRAP" || { echo "unexpected wrap dir in archive" >&2; exit 1; } echo "==> generating bindings with upstream binding_generator.py" ( cd "$WORK/$WRAP" + # generate_bindings() grew an `interface_filepath` parameter between + # godot-4.5-stable and 10.0.0-rc1, where gdextension_interface.h stopped + # being checked in and became generated from gdextension_interface.json. + # Dispatch on the actual signature and on which file the tag ships, rather + # than on the tag itself, so this keeps working across the change in both + # directions (cmake's GODOTCPP_GDEXTENSION_INTERFACE_FILE does the same). "$PYTHON" -c " +import inspect +import os import binding_generator -binding_generator.generate_bindings('gdextension/extension_api.json', True, '64', 'single', '.') + +params = inspect.signature(binding_generator.generate_bindings).parameters +if 'interface_filepath' in params: + interface = 'gdextension/gdextension_interface.json' + if not os.path.exists(interface): + interface = 'gdextension/gdextension_interface.h' + binding_generator.generate_bindings( + 'gdextension/extension_api.json', interface, + True, '64', 'single', '.') +else: + binding_generator.generate_bindings( + 'gdextension/extension_api.json', True, '64', 'single', '.') " # generate_bindings() imports the module, which leaves bytecode behind find . -name '__pycache__' -type d -prune -exec rm -rf {} +