将 Godot 原生引导迁移到官方 C++ 绑定
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 17s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 18s
Project CI / Backend tests (push) Failing after 17s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 18s
Project CI / AI game creator shell Rust crates (push) Failing after 18s
Project CI / Frontend tests (push) Failing after 6s
Project CI / AI game creator shell web tests (push) Failing after 11s
Project CI / Repository checks (push) Failing after 11s

使用固定版本 godot-cpp 替换手写 C ABI 引导并保留执行协议
采用 MSVC 与 CMake 构建,校验依赖归档和源码缓存
修复动态卸载时的实例绑定与单例包装生命周期
补充构建缓存测试、实机验收结果及分发文档
This commit is contained in:
kdletters
2026-09-20 22:00:44 +08:00
parent 429f991bde
commit fdc48aa573
14 changed files with 438 additions and 3529 deletions
+2 -1
View File
@@ -83,7 +83,8 @@ feature 会构建自包含 Attach helper,并只将运行文件与许可放入
[Unity 插件接入](../docs/technical/【技术方案】AGC Unity编辑器插件接入-2026-09-18.md)。
Godot 插件通过 `godot-editor-execute` feature 构建并校验 GDExtension 载荷,只分发
运行入口、DLL、元数据及许可。构建机需要 Windows x64 C 工具链;DLL 原件留在安装资源,
运行入口、DLL、元数据及许可。原生引导使用固定版本的官方 `godot-cpp`;构建机需要
Visual Studio C++ x64、CMake 和 Python,首次构建下载并校验绑定源码。DLL 原件留在安装资源,
每个编辑器的临时加载副本放在 AGC 私有缓存。连接时维护工程内受管 `.gdextension`
引用及其 UID,通过 Godot 聚焦扫描首次加载。文件归属、真实执行和卸载规则见
[Godot 插件接入](<../docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md>)。
@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.25)
project(agc_godot_editor LANGUAGES CXX)
if(NOT MSVC OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "Godot editor payload requires MSVC x64")
endif()
find_package(Python3 REQUIRED COMPONENTS Interpreter)
set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded CACHE STRING "Static runtime" FORCE)
set(GODOTCPP_TARGET editor CACHE STRING "Editor bindings" FORCE)
set(GODOTCPP_BUILD_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/build-profile.json" CACHE FILEPATH "Minimal bindings" FORCE)
set(GODOTCPP_USE_STATIC_CPP ON CACHE BOOL "Static runtime" FORCE)
set(GODOTCPP_USE_HOT_RELOAD OFF CACHE BOOL "Explicit unload/reload only" FORCE)
set(GODOTCPP_ENABLE_TESTING OFF CACHE BOOL "Do not package upstream tests" FORCE)
if(NOT EXISTS "${AGC_GODOT_CPP_SOURCE}/CMakeLists.txt")
message(FATAL_ERROR "Run build.ps1 to prepare the verified godot-cpp source")
endif()
add_subdirectory("${AGC_GODOT_CPP_SOURCE}" godot-cpp EXCLUDE_FROM_ALL SYSTEM)
add_library(agc_godot_editor SHARED src/native.cpp)
target_compile_features(agc_godot_editor PRIVATE cxx_std_17)
target_link_libraries(agc_godot_editor PRIVATE godot::cpp)
target_include_directories(agc_godot_editor PRIVATE "${AGC_GENERATED_DIR}")
target_compile_options(agc_godot_editor PRIVATE /W4 /WX /utf-8)
set_target_properties(agc_godot_editor PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/payload")
@@ -0,0 +1,3 @@
{
"enabled_classes": ["GDScript", "Node", "OS", "ProjectSettings"]
}
@@ -1,84 +1,73 @@
param([string]$Compiler = $env:AGC_GODOT_C_COMPILER)
param(
[string]$CMake = 'cmake.exe',
[string]$Python = 'python.exe',
[string]$Generator = 'Visual Studio 17 2022',
[ValidateRange(1, 32)][int]$Jobs = 4
)
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Godot editor native payload requires Windows x64.' }
if (-not [Environment]::Is64BitProcess) { throw 'A 64-bit build host is required.' }
if (-not $Compiler) {
foreach ($candidate in @('gcc.exe', 'clang.exe', 'cl.exe')) {
$found = Get-Command $candidate -ErrorAction SilentlyContinue
if ($found) { $Compiler = $found.Source; break }
}
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess) {
throw 'Godot editor payload requires Windows x64.'
}
if (-not $Compiler) { throw 'No C compiler found. Install a Windows x64 C toolchain or pass -Compiler.' }
$Compiler = (Get-Command $Compiler -ErrorAction Stop).Source
$build = Join-Path $root '.build'
$CMake = (Get-Command $CMake -ErrorAction Stop).Source
$Python = (Get-Command $Python -ErrorAction Stop).Source
$build = Join-Path $root '.build/msvc'
$generated = Join-Path $root '.build/generated'
$output = Join-Path $root 'bin/win-x64'
New-Item -ItemType Directory -Path $build,$output -Force | Out-Null
New-Item -ItemType Directory -Path $generated,$output -Force | Out-Null
$utf8 = [Text.UTF8Encoding]::new($false)
$inputs = @('src/native.c','src/bridge.gd','vendor/gdextension_interface.h','vendor/provenance.json','build.ps1')
$fingerprint = 'agc.godot.editor.v1/windows/x86_64/c11/O2' + "`n"
$fingerprint += 'compiler:' + (Get-FileHash -Algorithm SHA256 -LiteralPath $Compiler).Hash.ToLowerInvariant() + "`n"
foreach ($inputPath in $inputs) { $fingerprint += $inputPath + ':' + (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $root $inputPath)).Hash.ToLowerInvariant() + "`n" }
$hasher = [Security.Cryptography.SHA256]::Create()
try { $buildId = 'sha256:' + ([BitConverter]::ToString($hasher.ComputeHash($utf8.GetBytes($fingerprint))).Replace('-','').ToLowerInvariant()) } finally { $hasher.Dispose() }
$existingDll = Join-Path $output 'agc_godot_editor.dll'
$existingMetadata = Join-Path $output 'metadata.json'
if ((Test-Path -LiteralPath $existingDll) -and (Test-Path -LiteralPath $existingMetadata)) {
try {
$existing = Get-Content -LiteralPath $existingMetadata -Raw | ConvertFrom-Json
$existingHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $existingDll).Hash.ToLowerInvariant()
if ($existing.protocol -eq 'agc.godot.editor.v1' -and $existing.buildId -eq $buildId -and $existing.sha256 -eq $existingHash -and $existing.entrySymbol -eq 'agc_godot_editor_init' -and $existing.platform -eq 'windows' -and $existing.arch -eq 'x86_64' -and $existing.minimumGodotVersion -eq '4.7') {
Write-Output "Native payload is current: $buildId"
$previousBytecode = $env:PYTHONDONTWRITEBYTECODE
try {
$env:PYTHONDONTWRITEBYTECODE = '1'
$dependency = & $Python -X utf8 (Join-Path $root 'prepare_dependencies.py')
if ($LASTEXITCODE -ne 0) { throw 'Pinned godot-cpp dependency verification failed.' }
& $CMake -S $root -B $build -G $Generator -A x64 "-DAGC_GODOT_CPP_SOURCE=$dependency" "-DAGC_GENERATED_DIR=$generated" "-DPython3_EXECUTABLE=$Python"
if ($LASTEXITCODE -ne 0) { throw 'Godot C++ configure failed; install Visual Studio C++ x64, CMake and Python.' }
$compilerRecords = @(Get-ChildItem -LiteralPath (Join-Path $build 'CMakeFiles') -Filter 'CMakeCXXCompiler.cmake' -Recurse -File)
if ($compilerRecords.Count -ne 1) { throw 'MSVC compiler identity is ambiguous.' }
$record = [IO.File]::ReadAllText($compilerRecords[0].FullName)
$compilerMatch = [regex]::Match($record, 'set\(CMAKE_CXX_COMPILER "([^"]+)"\)')
if (-not $compilerMatch.Success -or -not $record.Contains('set(CMAKE_CXX_COMPILER_ID "MSVC")')) { throw 'MSVC compiler identity was not verified.' }
$compiler = $compilerMatch.Groups[1].Value
$inputs = @('src/native.cpp','src/bridge.gd','CMakeLists.txt','build-profile.json','prepare_dependencies.py','vendor/provenance.json','vendor/LICENSE.txt','build.ps1')
$fingerprint = 'agc.godot.editor.v1/windows/x86_64/c++17/msvc/Release/static-crt' + "`n"
$fingerprint += 'compiler:' + (Get-FileHash -LiteralPath $compiler -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
$fingerprint += 'cmake:' + (Get-FileHash -LiteralPath $CMake -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
$fingerprint += 'configuration:' + (Get-FileHash -LiteralPath (Join-Path $build 'CMakeCache.txt') -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
$fingerprint += 'toolchain:' + (Get-FileHash -LiteralPath $compilerRecords[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
foreach ($relative in $inputs) { $fingerprint += $relative + ':' + (Get-FileHash -LiteralPath (Join-Path $root $relative) -Algorithm SHA256).Hash.ToLowerInvariant() + "`n" }
$hasher = [Security.Cryptography.SHA256]::Create()
try { $buildId = 'sha256:' + ([BitConverter]::ToString($hasher.ComputeHash($utf8.GetBytes($fingerprint))).Replace('-','').ToLowerInvariant()) } finally { $hasher.Dispose() }
$dll = Join-Path $output 'agc_godot_editor.dll'
$metadataPath = Join-Path $output 'metadata.json'
if ((Test-Path -LiteralPath $dll) -and (Test-Path -LiteralPath $metadataPath)) {
$existing = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json
if ($existing.protocol -eq 'agc.godot.editor.v1' -and $existing.entrySymbol -eq 'agc_godot_editor_init' -and $existing.platform -eq 'windows' -and $existing.arch -eq 'x86_64' -and $existing.minimumGodotVersion -eq '4.7' -and $existing.buildId -eq $buildId -and $existing.sha256 -eq (Get-FileHash -LiteralPath $dll -Algorithm SHA256).Hash.ToLowerInvariant()) {
Write-Output "Native C++ payload is current: $buildId"
return
}
} catch { Write-Verbose 'Existing metadata could not be verified; rebuilding.' }
}
$script = [IO.File]::ReadAllBytes((Join-Path $root 'src/bridge.gd'))
$embedded = [Text.StringBuilder]::new()
[void]$embedded.AppendLine('/* Generated from src/bridge.gd; never reads a project-side script. */')
[void]$embedded.AppendLine('#define AGC_BUILD_ID "' + $buildId + '"')
[void]$embedded.AppendLine('static const unsigned char AGC_EMBEDDED_BRIDGE[] = {')
for ($index = 0; $index -lt $script.Length; $index += 32) {
$last = [Math]::Min($index + 31, $script.Length - 1)
[void]$embedded.AppendLine(($script[$index..$last] -join ',') + ',')
}
[void]$embedded.AppendLine('0};')
[IO.File]::WriteAllText((Join-Path $build 'embedded_bridge.h'), $embedded.ToString(), $utf8)
$previousTemp = $env:TEMP
$previousTmp = $env:TMP
$previousLocation = Get-Location
try {
$env:TEMP = $build
$env:TMP = $build
Set-Location -LiteralPath $build
$source = Join-Path $root 'src/native.c'
$vendor = Join-Path $root 'vendor'
$temporaryDll = Join-Path $build 'agc_godot_editor.dll'
$compilerName = [IO.Path]::GetFileName($Compiler).ToLowerInvariant()
if ($compilerName -eq 'cl.exe') {
& $Compiler /nologo /std:c11 /O2 /W4 /WX /LD /D_CRT_SECURE_NO_WARNINGS "/I$vendor" "/I$build" $source "/Fe:$temporaryDll" /link /Brepro
} else {
$flags = @('-std=c11','-O2','-Wall','-Wextra','-Werror','-shared')
if ($compilerName -eq 'gcc.exe') { $flags += @('-static-libgcc','-Wl,--no-insert-timestamp') }
& $Compiler @flags -I $vendor -I $build $source -o $temporaryDll
}
if ($LASTEXITCODE -ne 0) { throw "Native compiler exited with $LASTEXITCODE" }
$dll = Join-Path $output 'agc_godot_editor.dll'
Copy-Item -LiteralPath $temporaryDll -Destination $dll -Force
$script = [IO.File]::ReadAllBytes((Join-Path $root 'src/bridge.gd'))
$embedded = [Text.StringBuilder]::new()
[void]$embedded.AppendLine('#define AGC_BUILD_ID "' + $buildId + '"')
[void]$embedded.AppendLine('static const unsigned char AGC_EMBEDDED_BRIDGE[] = {')
for ($index = 0; $index -lt $script.Length; $index += 32) {
$last = [Math]::Min($index + 31, $script.Length - 1)
[void]$embedded.AppendLine(($script[$index..$last] -join ',') + ',')
}
[void]$embedded.AppendLine('0};')
$header = Join-Path $generated 'embedded_bridge.h'
if (-not (Test-Path -LiteralPath $header) -or [IO.File]::ReadAllText($header) -ne $embedded.ToString()) { [IO.File]::WriteAllText($header, $embedded.ToString(), $utf8) }
& $CMake --build $build --config Release --target agc_godot_editor --parallel $Jobs
if ($LASTEXITCODE -ne 0) { throw 'Godot C++ MSVC build failed.' }
Copy-Item -LiteralPath (Join-Path $build 'payload/agc_godot_editor.dll') -Destination $dll -Force
$metadata = [ordered]@{
protocol = 'agc.godot.editor.v1'
buildId = $buildId
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $dll).Hash.ToLowerInvariant()
platform = 'windows'
arch = 'x86_64'
entrySymbol = 'agc_godot_editor_init'
minimumGodotVersion = '4.7'
protocol = 'agc.godot.editor.v1'; buildId = $buildId
sha256 = (Get-FileHash -LiteralPath $dll -Algorithm SHA256).Hash.ToLowerInvariant()
platform = 'windows'; arch = 'x86_64'; entrySymbol = 'agc_godot_editor_init'; minimumGodotVersion = '4.7'
}
[IO.File]::WriteAllText((Join-Path $output 'metadata.json'), ($metadata | ConvertTo-Json) + "`n", $utf8)
Write-Output "Built $dll"
Write-Output "Build identity: $buildId"
[IO.File]::WriteAllText($metadataPath, ($metadata | ConvertTo-Json) + "`n", $utf8)
Write-Output "Built C++ payload: $buildId"
} finally {
Set-Location -LiteralPath $previousLocation
$env:TEMP = $previousTemp
$env:TMP = $previousTmp
$env:PYTHONDONTWRITEBYTECODE = $previousBytecode
}
@@ -0,0 +1,91 @@
"""Prepare only the pinned official SDK; never execute an unchecked archive."""
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import stat
import sys
import tempfile
import urllib.request
import zipfile
def plain(path):
for item in (path, *path.parents):
if item.exists() or item.is_symlink():
info = item.lstat()
if item.is_symlink() or getattr(info, "st_file_attributes", 0) & 0x400:
raise ValueError(f"Dependency path cannot contain links: {item}")
def digest(data):
return hashlib.sha256(data).hexdigest()
def prepare(root):
provenance = json.loads((root / "vendor/provenance.json").read_text(encoding="utf-8"))
commit = provenance["commit"]
expected = provenance["archiveSha256"]
if not re.fullmatch(r"[a-f0-9]{40}", commit) or not re.fullmatch(r"[a-f0-9]{64}", expected):
raise ValueError("Invalid pinned dependency identity")
url = f"https://codeload.github.com/godotengine/godot-cpp/zip/{commit}"
if provenance["archiveUrl"] != url:
raise ValueError("Dependency URL must identify the pinned official repository")
cache = root / ".build/dependencies"
plain(cache)
cache.mkdir(parents=True, exist_ok=True)
archive = cache / f"{commit}.zip"
plain(archive)
if not archive.exists():
with urllib.request.urlopen(url, timeout=60) as response:
data = response.read(32 * 1024 * 1024 + 1)
if len(data) > 32 * 1024 * 1024 or digest(data) != expected:
raise ValueError("Official godot-cpp archive SHA256 mismatch")
with tempfile.NamedTemporaryFile(dir=cache, delete=False) as output:
output.write(data)
temporary = Path(output.name)
os.replace(temporary, archive)
if digest(archive.read_bytes()) != expected:
raise ValueError("Cached godot-cpp archive SHA256 mismatch; cache was preserved")
source = cache / f"godot-cpp-{commit}"
plain(source)
source.mkdir(exist_ok=True)
expected_files = set()
with zipfile.ZipFile(archive) as bundle:
for entry in bundle.infolist():
relative = PurePosixPath(entry.filename)
if (not relative.parts or relative.parts[0] != source.name or relative.is_absolute()
or ".." in relative.parts or "\\" in entry.filename or ":" in entry.filename):
raise ValueError("Unsafe dependency archive entry")
if stat.S_ISLNK(entry.external_attr >> 16):
raise ValueError("Dependency archive cannot contain symbolic links")
if entry.is_dir():
continue
target = source.joinpath(*relative.parts[1:])
if target in expected_files:
raise ValueError("Duplicate dependency archive entry")
plain(target)
expected_files.add(target)
content = bundle.read(entry)
if target.exists():
if not target.is_file() or target.read_bytes() != content:
raise ValueError(f"Modified godot-cpp source cache was preserved: {target}")
else:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
for item in source.rglob("*"):
plain(item)
if item.is_file() and item not in expected_files:
raise ValueError(f"Unexpected dependency source cache entry: {item}")
if (source / "LICENSE.md").read_text(encoding="utf-8") != (root / "vendor/LICENSE.txt").read_text(encoding="utf-8"):
raise ValueError("Packaged godot-cpp license differs from pinned upstream license")
return source
if __name__ == "__main__":
try:
print(prepare(Path(__file__).resolve().parent))
except (OSError, ValueError, KeyError, zipfile.BadZipFile) as error:
print(str(error), file=sys.stderr)
sys.exit(1)
@@ -1,258 +0,0 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <wchar.h>
#include "gdextension_interface.h"
#include "embedded_bridge.h"
/* Windows x64 ABI storage is deliberately oversized and 16-byte aligned.
* Explicit C11 alignment also works with MSVC C, which lacks max_align_t.
* Objects are constructed/destructed solely through the official interface. */
typedef struct { _Alignas(16) unsigned char bytes[128]; } Storage;
static GDExtensionInterfacePrintWarning api_warning;
static GDExtensionInterfaceVariantCall api_call;
static GDExtensionInterfaceVariantDestroy api_destroy;
static GDExtensionInterfaceVariantGetType api_type;
static GDExtensionInterfaceGlobalGetSingleton api_singleton;
static GDExtensionInterfaceStringNameNewWithLatin1Chars api_name;
static GDExtensionInterfaceStringNewWithUtf8Chars api_string;
static GDExtensionInterfaceStringToUtf8Chars api_utf8;
static GDExtensionVariantFromTypeConstructorFunc from_object, from_string, from_name;
static GDExtensionTypeFromVariantConstructorFunc to_int, to_string;
static GDExtensionPtrDestructor destroy_name, destroy_string;
static Storage retained_script, retained_node;
static int script_live, node_live, started;
static void report_failure(const char *operation, int code) {
char message[256];
snprintf(message, sizeof(message), "AGC Godot editor bridge: %s failed (%d).", operation, code);
if (api_warning) api_warning(message, "agc_godot_editor", "native.c", 0, 0);
}
static void name_variant(Storage *out, const char *text) {
Storage name;
api_name(&name, text, 0);
from_name(out, &name);
destroy_name(&name);
}
static void string_variant(Storage *out, const char *text) {
Storage string;
api_string(&string, text);
from_string(out, &string);
destroy_string(&string);
}
static int invoke(Storage *receiver, const char *method,
const GDExtensionConstVariantPtr *arguments, int count, Storage *out) {
Storage name;
GDExtensionCallError error = { GDEXTENSION_CALL_OK, 0, 0 };
api_name(&name, method, 0);
api_call(receiver, &name, arguments, count, out, &error);
destroy_name(&name);
if (error.error != GDEXTENSION_CALL_OK) {
report_failure(method, (int)error.error);
return 0;
}
return 1;
}
static int singleton_variant(Storage *out, const char *text) {
Storage name;
api_name(&name, text, 0);
GDExtensionObjectPtr object = api_singleton(&name);
destroy_name(&name);
if (!object) return 0;
from_object(out, &object);
return 1;
}
static char *variant_utf8(Storage *value) {
if (api_type(value) != GDEXTENSION_VARIANT_TYPE_STRING) return NULL;
Storage string;
to_string(&string, value);
GDExtensionInt length = api_utf8(&string, NULL, 0);
char *text = NULL;
if (length >= 0 && length < 131072) {
text = (char *)malloc((size_t)length + 1);
if (text) {
api_utf8(&string, text, length);
text[length] = '\0';
}
}
destroy_string(&string);
return text;
}
static int plain_directory(const wchar_t *path) {
DWORD attrs = GetFileAttributesW(path);
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) &&
!(attrs & FILE_ATTRIBUTE_REPARSE_POINT);
}
static int ensure_cache_directory(wchar_t *path, size_t capacity, const wchar_t *part) {
size_t length = wcslen(path), addition = wcslen(part);
if (length + addition + 2 >= capacity) return 0;
if (length && path[length - 1] != L'\\') path[length++] = L'\\';
memcpy(path + length, part, (addition + 1) * sizeof(wchar_t));
if (!CreateDirectoryW(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) return 0;
return plain_directory(path);
}
static char *prepare_cache_path(void) {
Storage settings, argument, result;
if (!singleton_variant(&settings, "ProjectSettings")) return NULL;
string_variant(&argument, "res://");
const GDExtensionConstVariantPtr args[] = { &argument };
int ok = invoke(&settings, "globalize_path", args, 1, &result);
char *root_utf8 = ok ? variant_utf8(&result) : NULL;
api_destroy(&result);
api_destroy(&argument);
api_destroy(&settings);
if (!root_utf8) return NULL;
wchar_t path[32768];
int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, root_utf8, -1, path, 32768);
free(root_utf8);
if (length < 4 || path[1] != L':') return NULL;
for (int index = 0; index < length; ++index) if (path[index] == L'/') path[index] = L'\\';
/* Reject links/junctions in every existing directory, including project ancestors. */
for (int index = 3; index < length; ++index) {
if (path[index] != L'\\' && path[index] != L'\0') continue;
wchar_t saved = path[index];
path[index] = L'\0';
int plain = plain_directory(path);
path[index] = saved;
if (!plain) return NULL;
}
if (!ensure_cache_directory(path, 32768, L".godot") ||
!ensure_cache_directory(path, 32768, L"agc")) return NULL;
wchar_t suffix[96];
swprintf(suffix, 96, L"\\editor-bridge-%lu.json", (unsigned long)GetCurrentProcessId());
if (wcslen(path) + wcslen(suffix) + 1 >= 32768) return NULL;
wcscat(path, suffix);
DWORD attrs = GetFileAttributesW(path);
if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))) return NULL;
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, path, -1, NULL, 0, NULL, NULL);
if (size <= 0) return NULL;
char *cache = (char *)malloc((size_t)size);
if (cache) WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, path, -1, cache, size, NULL, NULL);
return cache;
}
static void release_references(void) {
if (node_live) { api_destroy(&retained_node); node_live = 0; }
if (script_live) { api_destroy(&retained_script); script_live = 0; }
}
static int schedule_bridge(void) {
FILETIME creation, exit_time, kernel, user;
if (!GetProcessTimes(GetCurrentProcess(), &creation, &exit_time, &kernel, &user)) return 0;
ULARGE_INTEGER timestamp;
timestamp.LowPart = creation.dwLowDateTime;
timestamp.HighPart = creation.dwHighDateTime;
char started_file_time[32];
snprintf(started_file_time, sizeof(started_file_time), "%llu", (unsigned long long)timestamp.QuadPart);
char *cache_path = prepare_cache_path();
if (!cache_path) { report_failure("session_cache_path", 0); return 0; }
Storage classdb, class_arg, result, source;
if (!singleton_variant(&classdb, "ClassDB")) { free(cache_path); return 0; }
name_variant(&class_arg, "GDScript");
const GDExtensionConstVariantPtr class_args[] = { &class_arg };
int ok = invoke(&classdb, "instantiate", class_args, 1, &retained_script);
script_live = 1;
api_destroy(&class_arg);
api_destroy(&classdb);
if (!ok || api_type(&retained_script) != GDEXTENSION_VARIANT_TYPE_OBJECT) { free(cache_path); return 0; }
string_variant(&source, (const char *)AGC_EMBEDDED_BRIDGE);
const GDExtensionConstVariantPtr source_args[] = { &source };
ok = invoke(&retained_script, "set_source_code", source_args, 1, &result);
api_destroy(&result);
api_destroy(&source);
if (!ok) { free(cache_path); return 0; }
ok = invoke(&retained_script, "reload", NULL, 0, &result);
int64_t reload_error = -1;
if (ok && api_type(&result) == GDEXTENSION_VARIANT_TYPE_INT) to_int(&reload_error, &result);
api_destroy(&result);
if (!ok || reload_error != 0) { free(cache_path); report_failure("bridge_compile", (int)reload_error); return 0; }
ok = invoke(&retained_script, "new", NULL, 0, &retained_node);
node_live = 1;
if (!ok || api_type(&retained_node) != GDEXTENSION_VARIANT_TYPE_OBJECT) { free(cache_path); return 0; }
Storage method, build, process_identity, cache;
name_variant(&method, "bootstrap");
string_variant(&build, AGC_BUILD_ID);
string_variant(&process_identity, started_file_time);
string_variant(&cache, cache_path);
free(cache_path);
const GDExtensionConstVariantPtr deferred[] = { &method, &build, &process_identity, &cache };
ok = invoke(&retained_node, "call_deferred", deferred, 4, &result);
api_destroy(&result);
api_destroy(&method);
api_destroy(&build);
api_destroy(&process_identity);
api_destroy(&cache);
return ok;
}
static void initialize_bridge(void *userdata, GDExtensionInitializationLevel level) {
(void)userdata;
if (level != GDEXTENSION_INITIALIZATION_EDITOR || started) return;
started = 1;
if (!schedule_bridge()) release_references();
}
static void deinitialize_bridge(void *userdata, GDExtensionInitializationLevel level) {
(void)userdata;
if (level != GDEXTENSION_INITIALIZATION_EDITOR) return;
if (node_live && api_type(&retained_node) == GDEXTENSION_VARIANT_TYPE_OBJECT) {
Storage returned;
invoke(&retained_node, "native_deinitialize", NULL, 0, &returned);
api_destroy(&returned);
}
release_references();
}
__declspec(dllexport) GDExtensionBool agc_godot_editor_init(
GDExtensionInterfaceGetProcAddress get_proc_address,
GDExtensionClassLibraryPtr library,
GDExtensionInitialization *initialization) {
(void)library;
if (!get_proc_address || !initialization) return 0;
#define LOAD(variable, type, symbol) do { \
GDExtensionInterfaceFunctionPtr raw_function = get_proc_address(symbol); \
_Static_assert(sizeof(type) == sizeof(raw_function), "Windows function pointer ABI mismatch"); \
memcpy(&(variable), &raw_function, sizeof(variable)); \
if (!variable) return 0; \
} while (0)
LOAD(api_warning, GDExtensionInterfacePrintWarning, "print_warning");
LOAD(api_call, GDExtensionInterfaceVariantCall, "variant_call");
LOAD(api_destroy, GDExtensionInterfaceVariantDestroy, "variant_destroy");
LOAD(api_type, GDExtensionInterfaceVariantGetType, "variant_get_type");
LOAD(api_singleton, GDExtensionInterfaceGlobalGetSingleton, "global_get_singleton");
LOAD(api_name, GDExtensionInterfaceStringNameNewWithLatin1Chars, "string_name_new_with_latin1_chars");
LOAD(api_string, GDExtensionInterfaceStringNewWithUtf8Chars, "string_new_with_utf8_chars");
LOAD(api_utf8, GDExtensionInterfaceStringToUtf8Chars, "string_to_utf8_chars");
GDExtensionInterfaceGetVariantFromTypeConstructor get_from;
GDExtensionInterfaceGetVariantToTypeConstructor get_to;
GDExtensionInterfaceVariantGetPtrDestructor get_destructor;
LOAD(get_from, GDExtensionInterfaceGetVariantFromTypeConstructor, "get_variant_from_type_constructor");
LOAD(get_to, GDExtensionInterfaceGetVariantToTypeConstructor, "get_variant_to_type_constructor");
LOAD(get_destructor, GDExtensionInterfaceVariantGetPtrDestructor, "variant_get_ptr_destructor");
#undef LOAD
from_object = get_from(GDEXTENSION_VARIANT_TYPE_OBJECT);
from_string = get_from(GDEXTENSION_VARIANT_TYPE_STRING);
from_name = get_from(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
to_int = get_to(GDEXTENSION_VARIANT_TYPE_INT);
to_string = get_to(GDEXTENSION_VARIANT_TYPE_STRING);
destroy_name = get_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
destroy_string = get_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
if (!from_object || !from_string || !from_name || !to_int || !to_string || !destroy_name || !destroy_string) return 0;
initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_EDITOR;
initialization->userdata = NULL;
initialization->initialize = initialize_bridge;
initialization->deinitialize = deinitialize_bridge;
return 1;
}
@@ -0,0 +1,159 @@
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <godot_cpp/classes/gd_script.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/project_settings.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/core/object.hpp>
#include <godot_cpp/godot.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <string>
#include "embedded_bridge.h"
namespace {
using namespace godot;
// Godot 值的析构必须发生在官方绑定终止之前,不能依赖 DLL 静态析构顺序。
struct BridgeState {
Ref<GDScript> script;
uint64_t node_id = 0;
};
BridgeState *bridge = nullptr;
bool plain_directory(const std::wstring &path) {
const DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_DIRECTORY) &&
!(attributes & FILE_ATTRIBUTE_REPARSE_POINT);
}
bool append_cache_directory(std::wstring &path, const wchar_t *part) {
if (path.back() != L'\\')
path += L'\\';
path += part;
if (!CreateDirectoryW(path.c_str(), nullptr) &&
GetLastError() != ERROR_ALREADY_EXISTS)
return false;
return plain_directory(path);
}
String session_cache_path() {
const String project_root =
ProjectSettings::get_singleton()->globalize_path("res://");
const Char16String utf16 = project_root.utf16();
static_assert(sizeof(wchar_t) == sizeof(char16_t),
"Windows UTF-16 path required");
std::wstring path(reinterpret_cast<const wchar_t *>(utf16.get_data()),
utf16.length());
if (path.size() < 3 || path.size() > 32000 || path[1] != L':')
return {};
for (wchar_t &character : path)
if (character == L'/')
character = L'\\';
// 项目根及其所有祖先都须为普通本地目录,不经过链接或 junction。
for (size_t index = 3; index <= path.size(); ++index) {
if (index == path.size() || path[index] == L'\\') {
if (!plain_directory(path.substr(0, index)))
return {};
}
}
if (!append_cache_directory(path, L".godot") ||
!append_cache_directory(path, L"agc"))
return {};
path +=
L"\\editor-bridge-" + std::to_wstring(GetCurrentProcessId()) + L".json";
const DWORD attributes = GetFileAttributesW(path.c_str());
if (attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)))
return {};
return String::utf16(reinterpret_cast<const char16_t *>(path.c_str()));
}
bool schedule_bridge() {
FILETIME creation, exit_time, kernel, user;
if (!GetProcessTimes(GetCurrentProcess(), &creation, &exit_time, &kernel,
&user))
return false;
ULARGE_INTEGER timestamp;
timestamp.LowPart = creation.dwLowDateTime;
timestamp.HighPart = creation.dwHighDateTime;
const String started = String::num_uint64(timestamp.QuadPart);
const String cache = session_cache_path();
if (cache.is_empty())
return false;
bridge->script.instantiate();
bridge->script->set_source_code(
String::utf8(reinterpret_cast<const char *>(AGC_EMBEDDED_BRIDGE)));
if (bridge->script->reload() != OK)
return false;
const Variant instance = bridge->script->call("new");
Node *node = Object::cast_to<Node>(static_cast<Object *>(instance));
if (!node)
return false;
bridge->node_id = node->get_instance_id();
node->call_deferred("bootstrap", AGC_BUILD_ID, started, cache);
return true;
}
void initialize_bridge(ModuleInitializationLevel level) {
if (level != MODULE_INITIALIZATION_LEVEL_EDITOR || bridge)
return;
bridge = new BridgeState;
if (!schedule_bridge()) {
UtilityFunctions::push_warning(
"AGC Godot editor bridge initialization failed");
delete bridge;
bridge = nullptr;
}
}
void deinitialize_bridge(ModuleInitializationLevel level) {
if (level != MODULE_INITIALIZATION_LEVEL_EDITOR)
return;
if (bridge) {
// bootstrap 失败或编辑器退出时 Node 可能先被销毁,不能保留悬空指针。
if (Object *node = ObjectDB::get_instance(bridge->node_id)) {
const GDExtensionObjectPtr owner = node->_owner;
node->call("native_deinitialize");
// GDScript 的 queue_free 晚于 DLL 卸载;只移除 C++ 包装的回调,不销毁引擎
// Node。
internal::gdextension_interface_object_free_instance_binding(
owner, internal::token);
}
// 脚本仍可能被当前 GDScript 调用栈引用。用 Variant 保活引擎对象,先释放 C++
// Ref, 再解除 DLL 内的包装回调;保活值在本回调返回前销毁。
const Variant script_lifetime = bridge->script;
const GDExtensionObjectPtr script_owner =
bridge->script.is_valid() ? bridge->script->_owner : nullptr;
bridge->script.unref();
if (script_owner)
internal::gdextension_interface_object_free_instance_binding(
script_owner, internal::token);
delete bridge;
bridge = nullptr;
}
// 晚加载扩展只收到 EDITOR 生命周期,官方单例包装清理原本位于 CORE 终止阶段。
ClassDB::deinitialize(GDEXTENSION_INITIALIZATION_CORE);
}
} // namespace
extern "C" GDExtensionBool GDE_EXPORT
agc_godot_editor_init(GDExtensionInterfaceGetProcAddress get_proc_address,
GDExtensionClassLibraryPtr library,
GDExtensionInitialization *initialization) {
godot::GDExtensionBinding::InitObject init(get_proc_address, library,
initialization);
init.register_initializer(initialize_bridge);
init.register_terminator(deinitialize_bridge);
init.set_minimum_library_initialization_level(
godot::MODULE_INITIALIZATION_LEVEL_EDITOR);
return init.init();
}
@@ -0,0 +1,73 @@
import hashlib
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
import zipfile
from unittest.mock import patch
sys.dont_write_bytecode = True
spec = importlib.util.spec_from_file_location("dependencies", Path(__file__).parents[1] / "prepare_dependencies.py")
dependencies = importlib.util.module_from_spec(spec)
spec.loader.exec_module(dependencies)
class DependencyTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.commit = "a" * 40
self.name = f"godot-cpp-{self.commit}"
self.archive = self.root / ".build/dependencies" / f"{self.commit}.zip"
self.archive.parent.mkdir(parents=True)
(self.root / "vendor").mkdir()
(self.root / "vendor/LICENSE.txt").write_bytes(b"MIT\r\n")
self.write_archive({"LICENSE.md": b"MIT\n", "src/core.cpp": b"verified"})
def write_archive(self, entries):
with zipfile.ZipFile(self.archive, "w") as output:
for name, content in entries.items():
output.writestr(f"{self.name}/{name}", content)
provenance = {"commit": self.commit,
"archiveUrl": f"https://codeload.github.com/godotengine/godot-cpp/zip/{self.commit}",
"archiveSha256": hashlib.sha256(self.archive.read_bytes()).hexdigest()}
(self.root / "vendor/provenance.json").write_text(json.dumps(provenance), encoding="utf-8")
def test_verified_archive_is_reusable_offline(self):
with patch("urllib.request.urlopen", side_effect=AssertionError("Unexpected network")):
source = dependencies.prepare(self.root)
self.assertEqual(source, dependencies.prepare(self.root))
self.assertEqual((source / "src/core.cpp").read_bytes(), b"verified")
def test_corrupt_archive_is_rejected_and_preserved(self):
self.archive.write_bytes(b"corrupt")
with self.assertRaisesRegex(ValueError, "SHA256 mismatch"):
dependencies.prepare(self.root)
self.assertEqual(self.archive.read_bytes(), b"corrupt")
def test_modified_source_is_rejected_even_with_valid_archive(self):
source = dependencies.prepare(self.root)
(source / "src/core.cpp").write_bytes(b"modified")
with self.assertRaisesRegex(ValueError, "Modified godot-cpp"):
dependencies.prepare(self.root)
self.assertEqual((source / "src/core.cpp").read_bytes(), b"modified")
def test_extra_source_is_rejected(self):
source = dependencies.prepare(self.root)
(source / "extra.cpp").write_text("unexpected", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unexpected dependency"):
dependencies.prepare(self.root)
def test_archive_paths_cannot_escape_on_windows_or_posix(self):
for name in ["../../escaped", "C:\\escaped", "..\\escaped"]:
with self.subTest(name=name):
self.write_archive({name: b"unexpected"})
with self.assertRaisesRegex(ValueError, "Unsafe dependency"):
dependencies.prepare(self.root)
if __name__ == "__main__":
unittest.main()
@@ -1,5 +1,6 @@
Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
# MIT License
Copyright (c) 2017-present Godot Engine contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,10 @@
{
"project": "Godot Engine",
"version": "4.7.2-stable",
"commit": "ed1daf0bf001b61586d9930840f2f1394092c079",
"project": "godot-cpp",
"version": "godot-4.5-stable",
"commit": "e83fd0904c13356ed1d4c3d09f8bb9132bdc6b77",
"license": "MIT",
"licenseFile": "LICENSE.txt",
"interfaceSource": "https://github.com/godotengine/godot/blob/ed1daf0bf001b61586d9930840f2f1394092c079/core/extension/gdextension_interface.json",
"headerGenerator": "https://github.com/godotengine/godot/blob/ed1daf0bf001b61586d9930840f2f1394092c079/core/extension/make_interface_header.py",
"generation": "Official unmodified generator using local file IO helpers; include guard and provenance comments added. No godot-cpp dependency."
"archiveUrl": "https://codeload.github.com/godotengine/godot-cpp/zip/e83fd0904c13356ed1d4c3d09f8bb9132bdc6b77",
"archiveSha256": "579af30c5f62c1084edb28216788188227d80a583f0114463699ac4edd22140f",
"generation": "Unmodified official C++ bindings and interface, generated using the checked-in minimal build profile and statically linked into the editor DLL."
}