port(2A): port/common Win32 layer, port_logic target and header gate

- Win32Types.h: fixed-width Win32 scalars, pointer-sized handles, static_asserts
- Win32Crt.{h,cpp}: MSVC _snprintf truncation contract, stricmp, timeGetTime wrap
- port_logic static lib linked into libmtgodot; standalone MTGODOT_BUILD_PORT build
- port_header_gate: every port/**.h compiles alone (twice) with its lib's
  40250 Distribute|Win32 defines (NDEBUG, USE_LOD, _DISTRIBUTE)
- script/port_gate.sh: macOS/Android/iOS/Windows(mingw) PASS, Linux BLOCKED

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-22 19:57:58 +09:00
co-authored by Claude Opus 5
parent 4521845a69
commit 2bae8d7644
11 changed files with 498 additions and 1 deletions
+4 -1
View File
@@ -56,6 +56,9 @@ target_include_directories(mtproto PUBLIC src/proto)
target_link_libraries(mtproto PUBLIC mt3p::sodium mt3p::minilzo)
target_compile_features(mtproto PUBLIC cxx_std_20)
# --- port_logic: 40250 logic mirror (extension/src/port, docs/PORT-PLAN.md) ---
add_subdirectory(src/port)
# Host-only CLIs + tests: they run on the build machine, so skip them entirely
# when cross-compiling the extension for a device.
set(MT_HOST_BUILD FALSE)
@@ -186,7 +189,7 @@ add_library(mtgodot ${MT_LIB_KIND}
src/proto/proto_node.cpp
)
target_compile_features(mtgodot PRIVATE cxx_std_20)
target_link_libraries(mtgodot PRIVATE godot::cpp xrender::libgr2 xrender::formats mtnet mtproto)
target_link_libraries(mtgodot PRIVATE godot::cpp xrender::libgr2 xrender::formats mtnet mtproto port_logic)
# Godot's .gdextension expects, per platform:
# project/bin/libmtgodot.macos.template_{debug,release}.dylib
+65
View File
@@ -0,0 +1,65 @@
# port_logic — mirror of the 40250 client logic (extension/src/port/<Lib>/<File>.{h,cpp}).
# No godot-cpp dependency: libmtgodot links it, and it also builds on its own (top-level
# -DMTGODOT_BUILD_EXTENSION=OFF -DMTGODOT_BUILD_PORT=ON) for targets without an extension build.
# See docs/PORT-PLAN.md batch 2A.
file(GLOB_RECURSE MT_PORT_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
file(GLOB_RECURSE MT_PORT_HEADERS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
add_library(port_logic STATIC ${MT_PORT_SOURCES})
target_include_directories(port_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(port_logic PUBLIC cxx_std_20)
if(WIN32)
target_link_libraries(port_logic PUBLIC winmm)
endif()
# 40250 Distribute|Win32 preprocessor definitions per library (vs_files/<Lib>/<Lib>.vcxproj),
# minus WIN32/_WINDOWS/_LIB/_CRT_SECURE_NO_WARNINGS: Windows-only branches go through platform/,
# never through ported logic. UserInterface/Locale_inc.h feature macros come with that header.
set(MT_PORT_DEFINES_default NDEBUG)
set(MT_PORT_DEFINES_GameLib NDEBUG USE_LOD)
set(MT_PORT_DEFINES_UserInterface NDEBUG USE_LOD _DISTRIBUTE)
function(mt_port_apply_defines file)
file(RELATIVE_PATH rel ${CMAKE_CURRENT_SOURCE_DIR} ${file})
string(REGEX REPLACE "/.*" "" lib "${rel}")
if(DEFINED MT_PORT_DEFINES_${lib})
set(defs ${MT_PORT_DEFINES_${lib}})
else()
set(defs ${MT_PORT_DEFINES_default})
endif()
set_property(SOURCE ${file} APPEND PROPERTY COMPILE_DEFINITIONS ${defs})
endfunction()
foreach(src IN LISTS MT_PORT_SOURCES)
mt_port_apply_defines(${src})
endforeach()
# Gate: every header under port/ compiles on its own (and twice, for its include guard), with its
# library's definitions. Built by default, so every platform build enforces it.
set(MT_PORT_GATE_SOURCES)
foreach(hdr IN LISTS MT_PORT_HEADERS)
file(RELATIVE_PATH rel ${CMAKE_CURRENT_SOURCE_DIR} ${hdr})
string(MAKE_C_IDENTIFIER "${rel}" id)
set(tu ${CMAKE_CURRENT_BINARY_DIR}/header_gate/${id}.cpp)
file(CONFIGURE OUTPUT ${tu} CONTENT "#include \"${rel}\"\n#include \"${rel}\"\n")
set_property(SOURCE ${tu} APPEND PROPERTY OBJECT_DEPENDS ${hdr})
list(APPEND MT_PORT_GATE_SOURCES ${tu})
# Definitions follow the header's library, not the generated file's location.
string(REGEX REPLACE "/.*" "" lib "${rel}")
if(DEFINED MT_PORT_DEFINES_${lib})
set_property(SOURCE ${tu} APPEND PROPERTY COMPILE_DEFINITIONS ${MT_PORT_DEFINES_${lib}})
else()
set_property(SOURCE ${tu} APPEND PROPERTY COMPILE_DEFINITIONS ${MT_PORT_DEFINES_default})
endif()
endforeach()
add_library(port_header_gate OBJECT ${MT_PORT_GATE_SOURCES})
target_include_directories(port_header_gate PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(port_header_gate PRIVATE cxx_std_20)
add_dependencies(port_logic port_header_gate)
if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
add_executable(port_common_test ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_common_test.cpp)
target_link_libraries(port_common_test PRIVATE port_logic)
add_test(NAME port.common COMMAND $<TARGET_FILE:port_common_test>)
endif()
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// Base of every ported library's slim StdAfx.h. 40250's EterBase/StdAfx.h pulls in <windows.h>
// and the STL headers below; the Windows part is replaced by Win32Types.h/Win32Crt.h. A library's
// own StdAfx.h adds only the headers audit/slices/*.json lists under `stdafx_supplied` for it.
#include "Win32Types.h"
#include "Win32Crt.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <time.h>
#include <algorithm>
#include <deque>
#include <list>
#include <map>
#include <string>
#include <vector>
+63
View File
@@ -0,0 +1,63 @@
#include "Win32Crt.h"
#if !defined(_WIN32)
#include <cctype>
#include <chrono>
#include <cstdio>
#include <vector>
char* _strlwr(char* s)
{
for (char* p = s; *p; ++p)
*p = static_cast<char>(std::tolower(static_cast<unsigned char>(*p)));
return s;
}
char* _strupr(char* s)
{
for (char* p = s; *p; ++p)
*p = static_cast<char>(std::toupper(static_cast<unsigned char>(*p)));
return s;
}
int _vsnprintf(char* buf, size_t count, const char* fmt, va_list ap)
{
va_list probe;
va_copy(probe, ap);
const int len = std::vsnprintf(nullptr, 0, fmt, probe);
va_end(probe);
if (len < 0)
return -1;
const size_t need = static_cast<size_t>(len);
if (need < count)
return std::vsnprintf(buf, count, fmt, ap);
std::vector<char> full(need + 1);
std::vsnprintf(full.data(), full.size(), fmt, ap);
std::memcpy(buf, full.data(), count);
return need == count ? len : -1;
}
int _snprintf(char* buf, size_t count, const char* fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int ret = _vsnprintf(buf, count, fmt, ap);
va_end(ap);
return ret;
}
DWORD timeGetTime()
{
using namespace std::chrono;
const auto ms = duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
return static_cast<DWORD>(static_cast<uint64_t>(ms));
}
DWORD GetTickCount()
{
return timeGetTime();
}
#endif // !_WIN32
+41
View File
@@ -0,0 +1,41 @@
#pragma once
// The Win32/MSVC-CRT functions and macros that 40250 logic code calls, with MSVC semantics.
// Added on demand as ported units need them; each one keeps the Windows behavior (including
// the legacy `_snprintf` truncation contract), not the nearest POSIX spelling.
#include "Win32Types.h"
#include <cstdarg>
#include <cstring>
#if !defined(_WIN32)
#include <strings.h>
#define ZeroMemory(dst, len) std::memset((dst), 0, (len))
#define FillMemory(dst, len, fill) std::memset((dst), (fill), (len))
#define CopyMemory(dst, src, len) std::memcpy((dst), (src), (len))
#define MoveMemory(dst, src, len) std::memmove((dst), (src), (len))
inline int _stricmp(const char* a, const char* b) { return strcasecmp(a, b); }
inline int stricmp(const char* a, const char* b) { return strcasecmp(a, b); }
inline int _strnicmp(const char* a, const char* b, size_t n) { return strncasecmp(a, b, n); }
inline int strnicmp(const char* a, const char* b, size_t n) { return strncasecmp(a, b, n); }
char* _strlwr(char* s);
char* _strupr(char* s);
// MSVC `_snprintf`/`_vsnprintf`: when the output is longer than `count`, `count` bytes are written
// with no terminator and -1 is returned; exactly `count` bytes are written unterminated and
// `count` is returned; otherwise the output is terminated and its length returned.
int _vsnprintf(char* buf, size_t count, const char* fmt, va_list ap);
int _snprintf(char* buf, size_t count, const char* fmt, ...)
#if defined(__GNUC__)
__attribute__((format(printf, 3, 4)))
#endif
;
// winmm `timeGetTime`: milliseconds since system start, wrapping at 2^32.
DWORD timeGetTime();
DWORD GetTickCount();
#endif // !_WIN32
+136
View File
@@ -0,0 +1,136 @@
#pragma once
// Win32 scalar and handle types with the widths the 40250 client was built with (32-bit Win32,
// ILP32). Scalars are fixed-width on every target; handle/pointer types stay pointer-sized and
// belong behind platform adapters. `long` itself cannot be redefined: ported code that relies on
// the 32-bit `long` uses LONG/DWORD or an explicit int32_t/uint32_t (docs/PORT-PLAN.md section 3).
#include <cstddef>
#include <cstdint>
#if defined(_WIN32)
// Windows (LLP64) already has these names with the same widths; use the SDK's so platform code
// that includes <windows.h> in the same translation unit does not conflict.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
typedef uint8_t BYTE;
typedef uint16_t WORD;
typedef uint32_t DWORD;
typedef int32_t LONG;
typedef uint32_t ULONG;
typedef int32_t BOOL;
typedef int32_t INT;
typedef uint32_t UINT;
typedef int16_t SHORT;
typedef uint16_t USHORT;
typedef char CHAR;
typedef uint8_t UCHAR;
typedef float FLOAT;
typedef int64_t LONGLONG;
typedef uint64_t ULONGLONG;
typedef int64_t INT64;
typedef uint64_t UINT64;
typedef uint64_t DWORD64;
typedef wchar_t WCHAR;
typedef void* LPVOID;
typedef const void* LPCVOID;
typedef void* PVOID;
typedef char* LPSTR;
typedef const char* LPCSTR;
typedef BYTE* LPBYTE;
typedef BYTE* PBYTE;
typedef WORD* LPWORD;
typedef DWORD* LPDWORD;
typedef DWORD* PDWORD;
typedef LONG* LPLONG;
typedef BOOL* LPBOOL;
typedef INT* LPINT;
typedef intptr_t INT_PTR;
typedef uintptr_t UINT_PTR;
typedef intptr_t LONG_PTR;
typedef uintptr_t ULONG_PTR;
typedef uintptr_t DWORD_PTR;
typedef uintptr_t WPARAM;
typedef intptr_t LPARAM;
typedef intptr_t LRESULT;
typedef void* HANDLE;
struct HWND__;
typedef HWND__* HWND;
struct HINSTANCE__;
typedef HINSTANCE__* HINSTANCE;
typedef HINSTANCE HMODULE;
struct HDC__;
typedef HDC__* HDC;
typedef struct tagPOINT
{
LONG x;
LONG y;
} POINT, *PPOINT, *LPPOINT;
typedef struct tagRECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT, *LPRECT;
typedef struct tagSIZE
{
LONG cx;
LONG cy;
} SIZE, *PSIZE, *LPSIZE;
#ifndef VOID
#define VOID void
#endif
#ifndef CONST
#define CONST const
#endif
#ifndef IN
#define IN
#endif
#ifndef OUT
#define OUT
#endif
#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif
#define CALLBACK
#define WINAPI
#define APIENTRY
#define __stdcall
#define __cdecl
#define __forceinline inline __attribute__((always_inline))
#define MAX_PATH 260
#define INFINITE 0xFFFFFFFF
#define LOBYTE(w) ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w) ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))
#define LOWORD(l) ((WORD)(((DWORD_PTR)(l)) & 0xffff))
#define HIWORD(l) ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff))
#define MAKEWORD(a, b) ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
#define MAKELONG(a, b) ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) | ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16))
#endif // _WIN32
// Widths the 40250 binary was compiled with; serialized structs depend on them.
static_assert(sizeof(BYTE) == 1 && sizeof(WORD) == 2 && sizeof(DWORD) == 4, "Win32 unsigned widths");
static_assert(sizeof(LONG) == 4 && sizeof(BOOL) == 4 && sizeof(UINT) == 4 && sizeof(INT) == 4, "Win32 signed widths");
static_assert(sizeof(SHORT) == 2 && sizeof(FLOAT) == 4, "Win32 short/float widths");
static_assert(sizeof(POINT) == 8 && sizeof(RECT) == 16, "Win32 POINT/RECT layout");
static_assert(sizeof(WPARAM) == sizeof(void*) && sizeof(HANDLE) == sizeof(void*), "handles stay pointer-sized");
+55
View File
@@ -0,0 +1,55 @@
// port/common: Win32 widths and MSVC CRT semantics the ported 40250 logic relies on.
#include "common/StdAfx.h"
#include <cstdio>
#include <cstdlib>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
int main()
{
// MSVC _snprintf: shorter output is terminated.
char buf[8];
std::memset(buf, 'x', sizeof(buf));
CHECK(_snprintf(buf, 8, "%s", "abc") == 3);
CHECK(std::strcmp(buf, "abc") == 0);
// Exactly `count` bytes: written unterminated, returns count.
std::memset(buf, 'x', sizeof(buf));
CHECK(_snprintf(buf, 4, "%s", "abcd") == 4);
CHECK(std::memcmp(buf, "abcdx", 5) == 0);
// Longer: `count` bytes written unterminated, returns -1.
std::memset(buf, 'x', sizeof(buf));
CHECK(_snprintf(buf, 4, "%d", 123456) == -1);
CHECK(std::memcmp(buf, "1234x", 5) == 0);
CHECK(stricmp("PLAYER", "player") == 0);
CHECK(_strnicmp("d:/ymir work/A", "D:/YMIR WORK/b", 12) == 0);
char s[] = "MixEd";
CHECK(std::strcmp(_strlwr(s), "mixed") == 0);
CHECK(MAKELONG(0x1234, 0xABCD) == static_cast<LONG>(0xABCD1234u));
CHECK(LOWORD(0xABCD1234u) == 0x1234 && HIWORD(0xABCD1234u) == 0xABCD);
CHECK(MAKEWORD(0x34, 0x12) == 0x1234);
// DWORD arithmetic wraps like the 32-bit original.
DWORD t = 0xFFFFFFF0u;
t += 0x20;
CHECK(t == 0x10);
const DWORD a = timeGetTime();
const DWORD b = timeGetTime();
CHECK(static_cast<DWORD>(b - a) < 1000);
if (g_failures)
return EXIT_FAILURE;
std::puts("port_common_test: ok");
return EXIT_SUCCESS;
}