diff --git a/.github/scripts/windows/build.bat b/.github/scripts/windows/build.bat new file mode 100644 index 0000000..b7beb3a --- /dev/null +++ b/.github/scripts/windows/build.bat @@ -0,0 +1,49 @@ +if /i "%GITHUB_ACTIONS%" neq "True" ( + echo for CI only + exit /b 3 +) + +set SDK_REMOTE=https://github.com/php/php-sdk-binary-tools.git +set SDK_BRANCH=%PHP_BUILD_SDK_BRANCH% +set SDK_RUNNER=%PHP_BUILD_CACHE_SDK_DIR%\phpsdk-%PHP_BUILD_CRT%-%PLATFORM%.bat + +if not exist "%PHP_BUILD_CACHE_BASE_DIR%" ( + echo Creating %PHP_BUILD_CACHE_BASE_DIR% + mkdir "%PHP_BUILD_CACHE_BASE_DIR%" +) + +if not exist "%PHP_BUILD_OBJ_DIR%" ( + echo Creating %PHP_BUILD_OBJ_DIR% + mkdir "%PHP_BUILD_OBJ_DIR%" +) + +if not exist "%SDK_RUNNER%" ( + if exist "%PHP_BUILD_CACHE_SDK_DIR%" rmdir /s /q "%PHP_BUILD_CACHE_SDK_DIR%" +) + +if not exist "%PHP_BUILD_CACHE_SDK_DIR%" ( + echo Cloning remote SDK repository + git clone --branch %SDK_BRANCH% %SDK_REMOTE% --depth 1 "%PHP_BUILD_CACHE_SDK_DIR%" 2>&1 +) + +for /f "tokens=*" %%a in ('type %PHP_BUILD_CACHE_SDK_DIR%\VERSION') do set GOT_SDK_VER=%%a +echo Got SDK version %GOT_SDK_VER% +if NOT "%GOT_SDK_VER%" == "%PHP_BUILD_SDK_BRANCH:~8%" ( + echo Switching to the configured SDK version %SDK_BRANCH:~8% + echo Fetching remote SDK repository + git --git-dir="%PHP_BUILD_CACHE_SDK_DIR%\.git" --work-tree="%PHP_BUILD_CACHE_SDK_DIR%" fetch --prune origin 2>&1 + echo Checkout SDK repository branch + git --git-dir="%PHP_BUILD_CACHE_SDK_DIR%\.git" --work-tree="%PHP_BUILD_CACHE_SDK_DIR%" checkout --force %SDK_BRANCH% +) + +if not exist "%SDK_RUNNER%" ( + echo "%SDK_RUNNER%" doesn't exist + exit /b 3 +) + +for /f "delims=" %%T in ('call .github\scripts\windows\find-vs-toolset.bat %PHP_BUILD_CRT%') do set "VS_TOOLSET=%%T" +echo Got VS Toolset %VS_TOOLSET% +cmd /c %SDK_RUNNER% -s %VS_TOOLSET% -t .github\scripts\windows\build_task.bat +if %errorlevel% neq 0 exit /b 3 + +exit /b 0 diff --git a/.github/scripts/windows/build_clickhouse_cpp.bat b/.github/scripts/windows/build_clickhouse_cpp.bat new file mode 100644 index 0000000..2f321fd --- /dev/null +++ b/.github/scripts/windows/build_clickhouse_cpp.bat @@ -0,0 +1,48 @@ +@echo off + +rem Build the bundled clickhouse-cpp as a static library together with its +rem contrib (cityhash, lz4, zstd, absl). Runs inside the phpsdk environment, so +rem the MSVC toolset (cl/nmake) is already on PATH. A single-config "NMake +rem Makefiles" generator drops each archive in its target's build directory, +rem which is exactly where config.w32 looks for them. Flags mirror the Linux CI +rem (.github/workflows/ci.yml): native Bool, no OpenSSL (TLS goes through PHP +rem streams), no tests/benchmarks. + +set CH_CPP_DIR=ext\clickhouse_async\third_party\clickhouse-cpp + +if not exist "%CH_CPP_DIR%\clickhouse\client.h" ( + echo ERROR: clickhouse-cpp submodule not found at %CH_CPP_DIR% + echo Run: git submodule update --init --recursive + exit /b 1 +) + +rem Reuse a previous build (the build directory is part of the runner cache). +if exist "%CH_CPP_DIR%\build\clickhouse\clickhouse-cpp-lib.lib" ( + echo clickhouse-cpp already built, skipping. + exit /b 0 +) + +rem clickhouse-cpp's bundled zstd unconditionally compiles a GAS assembly file +rem (decompress/huf_decompress_amd64.S) that MSVC cannot assemble, which breaks +rem the zstdstatic link. zstd falls back to its portable C path on MSVC +rem (ZSTD_ASM_SUPPORTED=0), so the asm source is not needed -- drop it. Patching +rem the freshly checked-out submodule here keeps our repo's submodule pointer +rem clean. Idempotent: the regex no longer matches once commented out. +powershell -NoProfile -Command "$f = '%CH_CPP_DIR%\contrib\zstd\zstd\CMakeLists.txt'; (Get-Content -LiteralPath $f) -replace '^\s*decompress/huf_decompress_amd64\.S\s*$', ' # huf_decompress_amd64.S excluded on MSVC (C fallback used)' | Set-Content -LiteralPath $f" +if %errorlevel% neq 0 exit /b 3 + +cmake -S "%CH_CPP_DIR%" -B "%CH_CPP_DIR%\build" -G "NMake Makefiles" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_C_COMPILER=cl ^ + -DCMAKE_CXX_COMPILER=cl ^ + -DBUILD_SHARED_LIBS=OFF ^ + -DCH_MAP_BOOL_TO_UINT8=OFF ^ + -DWITH_OPENSSL=OFF ^ + -DBUILD_TESTS=OFF ^ + -DBUILD_BENCHMARK=OFF +if %errorlevel% neq 0 exit /b 3 + +cmake --build "%CH_CPP_DIR%\build" +if %errorlevel% neq 0 exit /b 3 + +exit /b 0 diff --git a/.github/scripts/windows/build_task.bat b/.github/scripts/windows/build_task.bat new file mode 100644 index 0000000..8068da6 --- /dev/null +++ b/.github/scripts/windows/build_task.bat @@ -0,0 +1,58 @@ +@echo off + +if /i "%GITHUB_ACTIONS%" neq "True" ( + echo for CI only + exit /b 3 +) + +call %~dp0find-target-branch.bat +set STABILITY=staging +set DEPS_DIR=%PHP_BUILD_CACHE_BASE_DIR%\deps-%BRANCH%-%PHP_SDK_VS%-%PHP_SDK_ARCH% +rem SDK is cached, deps info is cached as well +echo Updating dependencies in %DEPS_DIR% +cmd /c phpsdk_deps --update --no-backup --branch %BRANCH% --stability %STABILITY% --deps %DEPS_DIR% --crt %PHP_BUILD_CRT% +if %errorlevel% neq 0 exit /b 3 + +rem Something went wrong, most likely when concurrent builds were to fetch deps +rem updates. It might be, that some locking mechanism is needed. +if not exist "%DEPS_DIR%" ( + cmd /c phpsdk_deps --update --force --no-backup --branch %BRANCH% --stability %STABILITY% --deps %DEPS_DIR% +) +if %errorlevel% neq 0 exit /b 3 + +rem Copy LibUV from vcpkg to deps directory (required by ext/async). +if not exist "%DEPS_DIR%\include\libuv" mkdir "%DEPS_DIR%\include\libuv" +if not exist "%DEPS_DIR%\lib" mkdir "%DEPS_DIR%\lib" +if not exist "%DEPS_DIR%\bin" mkdir "%DEPS_DIR%\bin" +copy "C:\vcpkg\installed\x64-windows\include\uv.h" "%DEPS_DIR%\include\libuv\uv.h" +xcopy /E /I /H /Y "C:\vcpkg\installed\x64-windows\include\uv" "%DEPS_DIR%\include\libuv\uv\" +copy "C:\vcpkg\installed\x64-windows\lib\uv.lib" "%DEPS_DIR%\lib\libuv.lib" +copy "C:\vcpkg\installed\x64-windows\bin\uv.dll" "%DEPS_DIR%\bin\uv.dll" + +rem Build the bundled clickhouse-cpp static library before configure: config.w32 +rem requires the prebuilt archives to be present. +call .github\scripts\windows\build_clickhouse_cpp.bat +if %errorlevel% neq 0 exit /b 3 + +cmd /c buildconf.bat --force +if %errorlevel% neq 0 exit /b 3 + +if "%THREAD_SAFE%" equ "0" set ADD_CONF=%ADD_CONF% --disable-zts +if "%INTRINSICS%" neq "" set ADD_CONF=%ADD_CONF% --enable-native-intrinsics=%INTRINSICS% + +cmd /c configure.bat ^ + --enable-snapshot-build ^ + --disable-debug-pack ^ + --without-analyzer ^ + --enable-object-out-dir=%PHP_BUILD_OBJ_DIR% ^ + --with-php-build=%DEPS_DIR% ^ + --enable-async ^ + --enable-clickhouse-async ^ + %ADD_CONF% ^ + --disable-test-ini +if %errorlevel% neq 0 exit /b 3 + +nmake /NOLOGO +if %errorlevel% neq 0 exit /b 3 + +exit /b 0 diff --git a/.github/scripts/windows/find-target-branch.bat b/.github/scripts/windows/find-target-branch.bat new file mode 100644 index 0000000..a0b47f2 --- /dev/null +++ b/.github/scripts/windows/find-target-branch.bat @@ -0,0 +1,8 @@ +@echo off + +for /f "usebackq tokens=3" %%i in (`findstr PHP_MAJOR_VERSION main\php_version.h`) do set BRANCH=%%i +for /f "usebackq tokens=3" %%i in (`findstr PHP_MINOR_VERSION main\php_version.h`) do set BRANCH=%BRANCH%.%%i + +if /i "%BRANCH%" equ "8.5" ( + set BRANCH=master +) diff --git a/.github/scripts/windows/find-vs-toolset.bat b/.github/scripts/windows/find-vs-toolset.bat new file mode 100644 index 0000000..ecaca67 --- /dev/null +++ b/.github/scripts/windows/find-vs-toolset.bat @@ -0,0 +1,52 @@ +@echo off + +setlocal enabledelayedexpansion + +if "%~1"=="" ( + echo ERROR: Usage: %~nx0 [vc14^|vc15^|vs16^|vs17^|vs18] + exit /b 1 +) + +set "toolsets_vc14=14.0" +set "toolsets_vc15=" +set "toolsets_vs16=" +set "toolsets_vs17=" +set "toolsets_vs18=" + + +for /f "usebackq tokens=*" %%I in (`vswhere.exe -latest -find "VC\Tools\MSVC"`) do set "MSVCDIR=%%I" + +if not defined MSVCDIR ( + echo ERROR: could not locate VC\Tools\MSVC + exit /b 1 +) + +for /f "delims=" %%D in ('dir /b /ad "%MSVCDIR%"') do ( + for /f "tokens=1,2 delims=." %%A in ("%%D") do ( + set "maj=%%A" & set "min=%%B" + if "!maj!"=="14" ( + if !min! LEQ 9 ( + set "toolsets_vc14=%%D" + ) else if !min! LEQ 19 ( + set "toolsets_vc15=%%D" + ) else if !min! LEQ 29 ( + set "toolsets_vs16=%%D" + ) else if !min! LEQ 49 ( + set "toolsets_vs17=%%D" + ) else ( + set "toolsets_vs18=%%D" + ) + ) + ) +) + +set "KEY=%~1" +set "VAR=toolsets_%KEY%" +call set "RESULT=%%%VAR%%%" +if defined RESULT ( + echo %RESULT% + exit /b 0 +) else ( + echo ERROR: no toolset found for %KEY% + exit /b 1 +) diff --git a/.github/scripts/windows/smoke.php b/.github/scripts/windows/smoke.php new file mode 100644 index 0000000..644140f --- /dev/null +++ b/.github/scripts/windows/smoke.php @@ -0,0 +1,19 @@ +nul + +echo. +echo --- php -m --- +"%PHP_BUILD_DIR%\php.exe" -n -m +echo. + +rem The extension is built statically into php.exe (no ini needed, -n). The +rem checks live in smoke.php so cmd does not mangle '!' and quotes in inline code. +echo --- verifying clickhouse_async --- +"%PHP_BUILD_DIR%\php.exe" -n "%~dp0smoke.php" +set RC=%errorlevel% + +echo. +echo smoke test exit code: %RC% +exit /b %RC% diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 0000000..03f7823 --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,73 @@ +name: ClickHouse Async Windows Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + WINDOWS: + name: WINDOWS_X64_ZTS + runs-on: windows-2025-vs2026 + timeout-minutes: 90 + env: + PHP_BUILD_CACHE_BASE_DIR: C:\build-cache + PHP_BUILD_OBJ_DIR: C:\obj + PHP_BUILD_CACHE_SDK_DIR: C:\build-cache\sdk + PHP_BUILD_SDK_BRANCH: php-sdk-2.7.1 + PHP_BUILD_CRT: vs18 + PLATFORM: x64 + THREAD_SAFE: "1" + INTRINSICS: AVX2 + PARALLEL: -j2 + OPCACHE: "1" + # php-src and php-async are separate repos whose engine ABI must match. + # These mirror the known-good pinned pair from .github/workflows/ci.yml; + # bump them together with ci.yml. + PHP_SRC_REPO: true-async/php-src + PHP_SRC_SHA: 568c83c6b307417d1f75690604353c65df865aa8 + PHP_ASYNC_REPO: true-async/php-async + PHP_ASYNC_SHA: 6b5aaa5d2ea90744a8c4e8c7c4e8d2b4cd24e14a + steps: + - name: git config + run: git config --global core.autocrlf false && git config --global core.eol lf + + - name: Checkout php-src to root + uses: actions/checkout@v4 + with: + repository: ${{ env.PHP_SRC_REPO }} + ref: ${{ env.PHP_SRC_SHA }} + + - name: Checkout async extension into ext/async + uses: actions/checkout@v4 + with: + repository: ${{ env.PHP_ASYNC_REPO }} + ref: ${{ env.PHP_ASYNC_SHA }} + path: ext/async + + - name: Checkout clickhouse extension (with clickhouse-cpp submodule) into ext/clickhouse_async + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + submodules: recursive + path: ext/clickhouse_async + + - name: Overlay clickhouse CI scripts onto the php-src tree + shell: cmd + run: xcopy /E /I /H /Y ext\clickhouse_async\.github .github + + - name: Setup LibUV (required by ext/async) + shell: powershell + run: | + if (!(Test-Path "C:\vcpkg")) { + git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg + C:\vcpkg\bootstrap-vcpkg.bat + } + C:\vcpkg\vcpkg.exe install libuv:x64-windows + + - name: Build (clickhouse-cpp + PHP with async and clickhouse_async) + run: .github/scripts/windows/build.bat + + - name: Smoke test (extension loads, Client class present) + run: .github/scripts/windows/test.bat diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 899be3a..28bda35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,9 @@ name: CI on: push: + branches: [main] pull_request: + branches: [main] jobs: test: diff --git a/ch_transport.cpp b/ch_transport.cpp index efc7a5e..ac5bec7 100644 --- a/ch_transport.cpp +++ b/ch_transport.cpp @@ -37,8 +37,13 @@ extern "C" { #include #include + +#ifdef PHP_WIN32 +/* winsock2.h / ws2tcpip.h are pulled in by above. */ +#else #include #include +#endif #include @@ -49,6 +54,24 @@ extern "C" { #define MSG_NOSIGNAL 0 #endif +/* The byte-movement paths below are identical across platforms except for how + * a socket reports "would block": POSIX sets errno (EAGAIN/EWOULDBLOCK), Winsock + * keeps its own WSAE* codes behind WSAGetLastError(), and recv/send take an int + * length on Windows. These shims absorb that difference. */ +#ifdef PHP_WIN32 +#define CH_SOCK_LAST_ERROR WSAGetLastError() +#define CH_SOCK_EINTR WSAEINTR +#define CH_SOCK_EAGAIN WSAEWOULDBLOCK +#define CH_SOCK_EWOULDBLOCK WSAEWOULDBLOCK +#define CH_SOCK_IOLEN(len) (static_cast(len)) +#else +#define CH_SOCK_LAST_ERROR errno +#define CH_SOCK_EINTR EINTR +#define CH_SOCK_EAGAIN EAGAIN +#define CH_SOCK_EWOULDBLOCK EWOULDBLOCK +#define CH_SOCK_IOLEN(len) (len) +#endif + namespace { /* @@ -103,7 +126,7 @@ class AsyncInput final : public clickhouse::InputStream { size_t DoRead(void *buf, size_t len) override { for (;;) { - ssize_t n = ::recv(fd_, static_cast(buf), len, 0); + ssize_t n = ::recv(fd_, static_cast(buf), CH_SOCK_IOLEN(len), 0); if (n > 0) { return static_cast(n); @@ -113,11 +136,11 @@ class AsyncInput final : public clickhouse::InputStream { throw chasync::ConnectionError("clickhouse: connection closed by peer"); } - if (errno == EINTR) { + if (CH_SOCK_LAST_ERROR == CH_SOCK_EINTR) { continue; } - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (CH_SOCK_LAST_ERROR == CH_SOCK_EAGAIN || CH_SOCK_LAST_ERROR == CH_SOCK_EWOULDBLOCK) { if (!ch_await_socket(fd_, ASYNC_READABLE, 0)) { throw chasync::ConnectionError("clickhouse: read interrupted"); } @@ -145,18 +168,18 @@ class AsyncOutput final : public clickhouse::OutputStream { size_t sent = 0; while (sent < len) { - ssize_t n = ::send(fd_, p + sent, len - sent, MSG_NOSIGNAL); + ssize_t n = ::send(fd_, p + sent, CH_SOCK_IOLEN(len - sent), MSG_NOSIGNAL); if (n > 0) { sent += static_cast(n); continue; } - if (errno == EINTR) { + if (CH_SOCK_LAST_ERROR == CH_SOCK_EINTR) { continue; } - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (CH_SOCK_LAST_ERROR == CH_SOCK_EAGAIN || CH_SOCK_LAST_ERROR == CH_SOCK_EWOULDBLOCK) { if (!ch_await_socket(fd_, ASYNC_WRITABLE, 0)) { throw chasync::ConnectionError("clickhouse: write interrupted"); } @@ -208,7 +231,10 @@ class AsyncSocket final : public clickhouse::Socket { private: void set_nonblocking() { -#ifndef PHP_WIN32 +#ifdef PHP_WIN32 + u_long nonblocking = 1; + ioctlsocket(handle_, FIONBIO, &nonblocking); +#else int flags = fcntl(handle_, F_GETFL, 0); if (flags >= 0) { fcntl(handle_, F_SETFL, flags | O_NONBLOCK); diff --git a/config.w32 b/config.w32 new file mode 100644 index 0000000..75d0a71 --- /dev/null +++ b/config.w32 @@ -0,0 +1,79 @@ +// vim:ft=javascript +// +// config.w32 for extension clickhouse_async +// +// Windows counterpart of config.m4. Native asynchronous ClickHouse client for +// PHP TrueAsync, built on the official clickhouse-cpp native-protocol library +// (bundled as a submodule under third_party/clickhouse-cpp). +// +// clickhouse-cpp must be built first as a static library with its bundled +// contrib (cityhash, lz4, zstd, absl) via CMake. Unlike GNU ld, the MSVC linker +// resolves inter-archive symbols across passes, so no --start-group is needed. +// See .github/scripts/windows/build_clickhouse_cpp.bat. + +ARG_ENABLE('clickhouse-async', 'Enable clickhouse_async support (native async ClickHouse client)', 'no'); + +if (PHP_CLICKHOUSE_ASYNC != 'no') { + + var ch_cpp_dir = configure_module_dirname + "\\third_party\\clickhouse-cpp"; + var ch_cpp_build = ch_cpp_dir + "\\build"; + + // clickhouse-cpp public headers live at the submodule root, e.g. + // ; this also verifies the submodule is checked out. + if (CHECK_HEADER_ADD_INCLUDE("clickhouse/client.h", "CFLAGS_CLICKHOUSE_ASYNC", ch_cpp_dir)) { + + // Static clickhouse-cpp + its bundled contrib, built by CMake with a + // single-config generator (NMake Makefiles, Release): the archive of + // each target lands in that target's build directory, mirroring the + // Unix layout in config.m4 (lib*.a -> *.lib). + var ch_libs = [ + ch_cpp_build + "\\clickhouse\\clickhouse-cpp-lib.lib", + ch_cpp_build + "\\contrib\\cityhash\\cityhash\\cityhash.lib", + ch_cpp_build + "\\contrib\\lz4\\lz4\\lz4.lib", + ch_cpp_build + "\\contrib\\zstd\\zstd\\zstdstatic.lib", + ch_cpp_build + "\\contrib\\absl\\absl\\absl_int128.lib" + ]; + + var ch_missing = null; + for (var i = 0; i < ch_libs.length; i++) { + if (!FSO.FileExists(ch_libs[i])) { + ch_missing = ch_libs[i]; + break; + } + } + + if (ch_missing == null) { + // C++17 + exceptions; absl (Int128) include from the contrib root. + // NOMINMAX: php.h pulls in before the clickhouse headers, + // and without it defines min()/max() macros that break + // clickhouse-cpp's std::max() usage (columns/array.h). The submodule + // root include and HAVE_CLICKHOUSE_CLIENT_H come from the + // CHECK_HEADER_ADD_INCLUDE above, so they are not repeated here. + ADD_FLAG("CFLAGS_CLICKHOUSE_ASYNC", "/std:c++17 /EHsc /DNOMINMAX /I \"" + ch_cpp_dir + "\\contrib\\absl\""); + + // clickhouse-cpp links Winsock on Windows; the prebuilt static + // archives carry the same dependency. + ADD_FLAG("LIBS", ch_libs.join(" ") + " ws2_32.lib"); + + EXTENSION("clickhouse_async", + "clickhouse_async.cpp ch_transport.cpp ch_exceptions.cpp ch_client.cpp", + false, + ""); + + // The async runtime (ext/async) registers the reactor this client + // drives at request time; load it first. + ADD_EXTENSION_DEP('clickhouse_async', 'async'); + + PHP_INSTALL_HEADERS("ext/clickhouse_async", "php_clickhouse_async.h"); + } else { + ERROR("clickhouse_async: prebuilt clickhouse-cpp static library not found (" + ch_missing + + ").\nBuild it first, e.g.:\n" + + " cmake -S third_party\\clickhouse-cpp -B third_party\\clickhouse-cpp\\build -G \"NMake Makefiles\" ^\n" + + " -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCH_MAP_BOOL_TO_UINT8=OFF ^\n" + + " -DWITH_OPENSSL=OFF -DBUILD_TESTS=OFF -DBUILD_BENCHMARK=OFF\n" + + " cmake --build third_party\\clickhouse-cpp\\build"); + } + } else { + ERROR("clickhouse_async: clickhouse-cpp submodule not found. Run: git submodule update --init --recursive"); + } +} diff --git a/php_clickhouse_async.h b/php_clickhouse_async.h index eca45ad..5fccfe7 100644 --- a/php_clickhouse_async.h +++ b/php_clickhouse_async.h @@ -11,7 +11,20 @@ #ifndef PHP_CLICKHOUSE_ASYNC_H #define PHP_CLICKHOUSE_ASYNC_H +/* This translation unit is C++, but the engine references the module entry as + * a C symbol (e.g. from the generated internal_functions table of a static + * build). Give it C linkage so the mangled C++ name does not cause an + * unresolved external at link time. Harmless for the shared build. */ +#ifdef __cplusplus +extern "C" { +#endif + extern zend_module_entry clickhouse_async_module_entry; + +#ifdef __cplusplus +} +#endif + #define phpext_clickhouse_async_ptr &clickhouse_async_module_entry #define PHP_CLICKHOUSE_ASYNC_VERSION "0.1.0-dev"