diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 40edbd37..5337f3fd 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -386,7 +386,7 @@ jobs: throw "Windows smoke executable failed with exit code $($process.ExitCode)" } if ($stdout.Trim() -ne 'windows-smoke-ok:zts') { - throw "Unexpected Windows smoke output: $stdout" + throw "Unexpected Windows smoke output: $stdout" } - name: Run Windows Nano smoke test @@ -397,7 +397,8 @@ jobs: $env:PHPRC = Join-Path $env:PHP_HOME 'php.ini' $env:PATH = "$env:PHPX_HOME\build;$env:PATH" $nanoExe = Join-Path '${{ github.workspace }}' 'tests\windows\smoke\windows_nano_smoke.exe' - & .\tpc.exe tests\windows\smoke\project.yml --nano --output $nanoExe --job 1 --no-progress + $nanoBuildDir = Join-Path $env:RUNNER_TEMP 'typephp-windows-nano-console-build' + & .\tpc.exe tests\windows\smoke\project.yml --nano --build-dir $nanoBuildDir --output $nanoExe --job 1 --no-progress if ($LASTEXITCODE -ne 0) { throw "Windows Nano smoke project compilation failed with exit code $LASTEXITCODE" } @@ -407,12 +408,22 @@ jobs: throw "Windows Nano smoke executable was not generated: $nanoExe" } + $nanoImports = (& dumpbin.exe /imports $nanoExe | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "dumpbin failed for Windows Nano executable with exit code $LASTEXITCODE" + } + if ($nanoImports -match '(?im)^\s*php(?:x|\d.*)?\.dll\s*$') { + throw "Windows Nano executable imports a PHP/PHPX runtime DLL:`n$nanoImports" + } + $processInfo = [Diagnostics.ProcessStartInfo]::new() $processInfo.FileName = $nanoExe - $processInfo.ArgumentList.Add('zts') + $processInfo.ArgumentList.Add('nts') $processInfo.UseShellExecute = $false $processInfo.RedirectStandardOutput = $true $processInfo.RedirectStandardError = $true + $processInfo.Environment['PATH'] = "$env:SystemRoot\System32;$env:SystemRoot" + $processInfo.Environment.Remove('PHPRC') $process = [Diagnostics.Process]::new() $process.StartInfo = $processInfo @@ -429,26 +440,32 @@ jobs: if ($process.ExitCode -ne 0) { throw "Windows Nano smoke executable failed with exit code $($process.ExitCode)" } - if ($stdout.Trim() -ne 'windows-smoke-ok:zts') { - throw "Unexpected Windows Nano smoke output: $stdout" + if ($stdout.Trim() -ne 'windows-smoke-ok:nts') { + throw "Unexpected Windows Nano smoke output: $stdout" } $policyInfo = [Diagnostics.ProcessStartInfo]::new() $policyInfo.FileName = $nanoExe - $policyInfo.ArgumentList.Add('zts') + $policyInfo.ArgumentList.Add('nts') $policyInfo.ArgumentList.Add('nano') $policyInfo.UseShellExecute = $false $policyInfo.RedirectStandardOutput = $true $policyInfo.RedirectStandardError = $true + $policyInfo.Environment['PATH'] = "$env:SystemRoot\System32;$env:SystemRoot" + $policyInfo.Environment.Remove('PHPRC') $policyProcess = [Diagnostics.Process]::new() $policyProcess.StartInfo = $policyInfo if (-not $policyProcess.Start()) { throw 'Unable to start the Windows Nano policy probe' } + if (-not $policyProcess.WaitForExit(30000)) { + $policyProcess.Kill() + $policyProcess.WaitForExit() + throw 'Windows Nano policy probe timed out' + } $policyStdout = $policyProcess.StandardOutput.ReadToEnd() $policyStderr = $policyProcess.StandardError.ReadToEnd() - $policyProcess.WaitForExit() Write-Host "Windows Nano policy stdout: $policyStdout" Write-Host "Windows Nano policy stderr: $policyStderr" @@ -457,15 +474,16 @@ jobs: throw 'Windows Nano policy probe unexpectedly succeeded' } $policyOutput = "$policyStdout`n$policyStderr" - if (-not $policyOutput.Contains('Function `exec` is not supported in nano mode')) { + if (-not $policyOutput.Contains("function 'call_user_func' is undefined.")) { throw "Windows Nano policy probe returned an unexpected error: $policyOutput" } - # A GUI-subsystem Nano application uses the same generated entry and - # DLL backend, but must also link without a console window. + # A GUI-subsystem Nano application uses the same source-composed + # runtime, but must also link without a console window. $nanoGuiExe = Join-Path '${{ github.workspace }}' 'tests\windows\smoke\windows_nano_gui_smoke.exe' + $nanoGuiBuildDir = Join-Path $env:RUNNER_TEMP 'typephp-windows-nano-gui-build' $env:PHPRC = Join-Path $env:PHP_HOME 'php.ini' - & .\tpc.exe tests\windows\smoke\project.yml --nano --no-console --output $nanoGuiExe --job 1 --no-progress + & .\tpc.exe tests\windows\smoke\project.yml --nano --no-console --build-dir $nanoGuiBuildDir --output $nanoGuiExe --job 1 --no-progress if ($LASTEXITCODE -ne 0) { throw "Windows Nano GUI project compilation failed with exit code $LASTEXITCODE" } @@ -473,6 +491,14 @@ jobs: if (-not (Test-Path $nanoGuiExe)) { throw "Windows Nano GUI executable was not generated: $nanoGuiExe" } + $guiImports = (& dumpbin.exe /imports $nanoGuiExe | Out-String) + if ($guiImports -match '(?im)^\s*php(?:x|\d.*)?\.dll\s*$') { + throw "Windows Nano GUI executable imports a PHP/PHPX runtime DLL:`n$guiImports" + } + $guiHeaders = (& dumpbin.exe /headers $nanoGuiExe | Out-String) + if ($guiHeaders -notmatch '(?im)^\s*2\s+subsystem\b') { + throw "Windows Nano GUI executable is not linked with the Windows GUI subsystem:`n$guiHeaders" + } - name: Package tested Windows compiler if: startsWith(github.ref, 'refs/tags/') diff --git a/README-CN.md b/README-CN.md index c499caa6..7de62a4b 100644 --- a/README-CN.md +++ b/README-CN.md @@ -243,20 +243,19 @@ string(16) "Linux ..." 默认可执行文件生成在执行 `tpc` 时的当前目录;普通模式与 Nano 模式共用 `build` 目录保存生成代码、目标文件等中间产物。可使用 `-o` 显式修改输出路径。 -PHP 与 Composer 仅用于编译期。在 Linux、macOS、iOS、Android 上,生成的程序 -使用静态选定的 Nano 运行时及仅文件模式的 stream。Native Nano 可使用 C11、 -C++17 与 POSIX.1-2008,但依然不提供 socket、DNS、网络、远程 stream、动态 PHP -加载及进程执行能力。WASI 是更小的能力子集,直接调用目标不支持的 API 会在 -编译期报错。 +PHP 与 Composer 仅用于编译期。在 Windows、Linux、macOS、iOS、Android 上, +生成程序都会把选中的 PHP Nano 与 PHPX 源码直接编译进最终 exe 或库,不导入 +`php.dll`、`phpx.dll`,也不依赖宿主 `libphp`/`libphpx`。运行时使用仅文件模式的 +stream,依然不提供 socket、DNS、网络、远程 stream、动态 PHP 加载及进程执行 +能力。WASI 是更小的能力子集,直接调用目标不支持的 API 会在编译期报错。 所有平台的 `--nano` 都会拒绝 `eval`、`include`、`include_once`、`require`、 `require_once` 等 VM 入口以及匿名类。 -Windows 通过独立构建后端支持 `mode: bin` 和 `mode: lib` 的 Nano 原生应用:它仍走 -宿主机编译、链接流程,通过 import library 连接 `php.dll` 与 `phpx.dll`。Windows 不加载 -`swoole/php-nano`、`swoole/phpx` 的源码清单,也不会把它们的 C/C++ 源文件加入 -项目 `sources`。外部命令 API 与反引号语法依然会被拒绝;请求启动时还会从 Zend -函数表移除这些命令函数,避免变量函数或回调形式绕过编译期检查。 +Windows 的 `mode: bin` 和 `mode: lib` 与 Linux、macOS 使用相同的源码组合契约: +MSVC 将 C11 PHP Nano、C++17 PHPX 与生成的 TypePHP 源码连接为同一个 PE 产物; +仅允许保留正常的 Windows 系统库和编译器运行库依赖。外部命令 API 与反引号语法 +在所有平台均由相同的 Nano 能力策略拒绝。 除运行时 sources、头文件目录、编译宏和链接输入外,Nano 与普通模式共用同一套 命令行参数解析、TypePHP 代码生成、并行任务调度、编译进度条、输出路径规则以及 diff --git a/README.md b/README.md index a8866bf9..b0305349 100644 --- a/README.md +++ b/README.md @@ -277,24 +277,23 @@ By default, the executable is emitted in the directory where `tpc` was invoked. Normal and Nano builds share the `build` directory for generated code, objects, and other intermediate files. Use `-o` to select a different output path. -PHP and Composer remain build-time tools. On Linux, macOS, iOS, and Android, -the generated program uses the statically selected Nano runtime and its -file-only stream layer. Native Nano may use C11, C++17, and POSIX.1-2008, but -socket/DNS/network, remote streams, dynamic PHP loading, and process execution -remain unavailable. WASI is a smaller subset; direct calls to APIs missing from -that target are compile-time errors. +PHP and Composer remain build-time tools. On Windows, Linux, macOS, iOS, and +Android, the generated program compiles the selected PHP Nano and PHPX sources +directly into the final executable or library. It does not import `php.dll`, +`phpx.dll`, or a host `libphp`/`libphpx`. The runtime retains its file-only +stream layer; socket/DNS/network, remote streams, dynamic PHP loading, and +process execution remain unavailable. WASI is a smaller subset; direct calls +to APIs missing from that target are compile-time errors. On every platform, `--nano` rejects the VM entry paths `eval`, `include`, `include_once`, `require`, and `require_once`, as well as anonymous classes. -Windows supports Nano native applications in `mode: bin` and `mode: lib` through -a different build backend: it keeps the existing host compile/link pipeline and -connects to `php.dll` and `phpx.dll` through their import libraries. It does not -load the `swoole/php-nano` or -`swoole/phpx` source manifests, nor append their C/C++ files to project `sources`. -External-command APIs and backtick syntax are still rejected. Those command -functions are also removed from the Zend function table at request startup, so -indirect variable/callback calls cannot bypass the policy. +Windows supports Nano native applications in `mode: bin` and `mode: lib` with +the same source-composition contract as Linux and macOS. MSVC compiles the C11 +PHP Nano sources, the C++17 PHPX sources, and generated TypePHP sources into one +PE artifact; only Windows system and compiler-runtime DLLs may remain as normal +platform dependencies. External-command APIs and backtick syntax are rejected +by the same Nano capability policy on every platform. Except for runtime sources, include directories, compile definitions, and link inputs, Nano and normal mode share command-line parsing, TypePHP code generation, diff --git a/composer.json b/composer.json index e83be439..c6782a21 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpunit": "^10.4", "friendsofphp/php-cs-fixer": "^3.40", "phpstan/phpstan": "^2.2", - "swoole/php-nano": "^1.0.2" + "swoole/php-nano": "^1.1" }, "autoload": { "psr-4": { diff --git a/phpunit/src/Backend/BackendOptionsTest.php b/phpunit/src/Backend/BackendOptionsTest.php index 16c46b61..60b8e2cf 100644 --- a/phpunit/src/Backend/BackendOptionsTest.php +++ b/phpunit/src/Backend/BackendOptionsTest.php @@ -37,6 +37,19 @@ class BackendOptionsTest extends TestCase $this->assertStringContainsString('/nologo', $options); } + public function testMsvcNanoSectionGarbageCollectionOptions(): void + { + $compiler = new Msvc(new Windows()); + + $compileOptions = $compiler->buildCompileOptions(['section_gc' => true]); + self::assertStringContainsString('/Gy', $compileOptions); + self::assertStringContainsString('/Gw', $compileOptions); + + $linkOptions = $compiler->buildLinkOptions(['section_gc' => true]); + self::assertStringContainsString('/OPT:REF', $linkOptions); + self::assertStringContainsString('/OPT:ICF', $linkOptions); + } + /** * 测试 MSVC 编译选项 - ZTS 模式 */ diff --git a/phpunit/src/Build/ComposerNativePackageTest.php b/phpunit/src/Build/ComposerNativePackageTest.php index 1a2a644c..a3378890 100644 --- a/phpunit/src/Build/ComposerNativePackageTest.php +++ b/phpunit/src/Build/ComposerNativePackageTest.php @@ -72,6 +72,29 @@ final class ComposerNativePackageTest extends TestCase ); } + public function testLoadsWindowsPlatformSourcesIncludesAndDefines(): void + { + $this->installFixture(true); + mkdir($this->directory . '/windows'); + file_put_contents($this->directory . '/src/windows.c', 'int typephp_windows(void) { return 1; }'); + + $manifestPath = $this->directory . '/composer.json'; + $manifest = json_decode((string) file_get_contents($manifestPath), true, flags: JSON_THROW_ON_ERROR); + $manifest['extra']['typephp-native']['defines'] = ['COMMON=1']; + $manifest['extra']['typephp-native']['platforms']['windows'] = [ + 'sources' => ['src/windows.c'], + 'include-dirs' => ['windows'], + 'defines' => ['WINDOWS_NATIVE=1'], + ]; + file_put_contents($manifestPath, json_encode($manifest, JSON_THROW_ON_ERROR)); + + $package = ComposerNativePackage::load('swoole/php-ext-example', null, 'Windows'); + + self::assertContains(realpath($this->directory . '/src/windows.c'), $package->sources); + self::assertContains(realpath($this->directory . '/windows'), $package->includeDirs); + self::assertSame(['COMMON=1', 'WINDOWS_NATIVE=1'], $package->defines); + } + private function installFixture(bool $requireRuntime): void { $manifest = [ diff --git a/phpunit/src/Build/NanoBuildBackendTest.php b/phpunit/src/Build/NanoBuildBackendTest.php index 8de617c0..75b91f08 100644 --- a/phpunit/src/Build/NanoBuildBackendTest.php +++ b/phpunit/src/Build/NanoBuildBackendTest.php @@ -7,22 +7,17 @@ use TypePhp\Build\NanoBuildBackend; final class NanoBuildBackendTest extends TestCase { - public function testWindowsKeepsTheHostDllBuildBackend(): void - { - self::assertSame(NanoBuildBackend::WINDOWS_DLL, NanoBuildBackend::forHost('Windows')); - self::assertFalse(NanoBuildBackend::composesRuntimeSources('Windows')); - } - - /** @dataProvider nonWindowsHosts */ - public function testNonWindowsHostsComposeComposerRuntimeSources(string $osFamily): void + /** @dataProvider nativeHosts */ + public function testEveryNativeHostComposesComposerRuntimeSources(string $osFamily): void { self::assertSame(NanoBuildBackend::COMPOSER_SOURCES, NanoBuildBackend::forHost($osFamily)); self::assertTrue(NanoBuildBackend::composesRuntimeSources($osFamily)); } - public static function nonWindowsHosts(): array + public static function nativeHosts(): array { return [ + ['Windows'], ['Linux'], ['Darwin'], ['BSD'], diff --git a/phpunit/src/Build/NativeDependencyAuditorTest.php b/phpunit/src/Build/NativeDependencyAuditorTest.php index 2280c3bb..4ac9ca9c 100644 --- a/phpunit/src/Build/NativeDependencyAuditorTest.php +++ b/phpunit/src/Build/NativeDependencyAuditorTest.php @@ -146,4 +146,30 @@ final class NativeDependencyAuditorTest extends TestCase ); self::addToAssertionCount(1); } + + public function testWindowsSystemRuntimeImportsAreAllowed(): void + { + (new NativeDependencyAuditor())->assertWindowsImports( + " KERNEL32.dll\n VCRUNTIME140.dll\n ucrtbase.dll\n", + ); + self::addToAssertionCount(1); + } + + public function testWindowsPhpRuntimeDllIsRejected(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('phpx.dll'); + (new NativeDependencyAuditor())->assertWindowsImports( + " KERNEL32.dll\n phpx.dll\n", + ); + } + + public function testWindowsForbiddenCapabilityImportIsRejected(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('socket'); + (new NativeDependencyAuditor())->assertWindowsImports( + " WS2_32.dll\n 123 socket\n", + ); + } } diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 29ddbb78..4e5073e6 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -1348,7 +1348,7 @@ YAML); $this->invokeMethod('applyCommandLineArguments'); } - public function testWindowsNanoUsesNativeDllBackendAndRejectsExtensionMode(): void + public function testWindowsNanoComposesRuntimeSourcesAndRejectsExtensionMode(): void { global $argv; $argv = ['compiler.php', '--nano']; @@ -1364,7 +1364,7 @@ YAML); $apply->invoke($compiler); self::assertTrue($compiler->isNanoPolicyMode()); - self::assertFalse($compiler->isNanoMode()); + self::assertTrue($compiler->isNanoMode()); $buildMode = $reflection->getProperty('buildMode'); $buildMode->setAccessible(true); diff --git a/phpunit/src/NanoCapabilityPolicyTest.php b/phpunit/src/NanoCapabilityPolicyTest.php index be66de23..7b140bd0 100644 --- a/phpunit/src/NanoCapabilityPolicyTest.php +++ b/phpunit/src/NanoCapabilityPolicyTest.php @@ -23,7 +23,7 @@ final class NanoCapabilityPolicyCompiler extends CompilerTest $this->file = 'nano-policy.php'; } - public function enableFullRuntimeNanoPolicyForTest(): void + public function enableNanoPolicyWithoutRuntimeForTest(): void { $this->nanoMode = false; $this->nanoPolicyMode = true; @@ -108,12 +108,12 @@ final class NanoCapabilityPolicyTest extends BaseTest } } - public function testFullRuntimeNanoPolicyRejectsExternalCommandsOnly(): void + public function testNanoPolicyAlwaysUsesSourceRuntimeCapabilitySet(): void { $compiler = new NanoCapabilityPolicyCompiler(TYPEPHP_ROOT_PATH); - $compiler->enableFullRuntimeNanoPolicyForTest(); + $compiler->enableNanoPolicyWithoutRuntimeForTest(); - foreach (['exec', 'passthru', 'pcntl_exec', 'popen', 'proc_open', 'proc_terminate', 'shell_exec', 'system'] as $name) { + foreach (['exec', 'getenv', 'parse_str', 'stream_socket_client'] as $name) { try { $compiler->validateNanoFunction($name); self::fail("{$name} was accepted"); @@ -122,73 +122,6 @@ final class NanoCapabilityPolicyTest extends BaseTest } } - // Windows uses the complete PHP/PHPX DLL runtime. The php-nano-only - // capability reductions are therefore not applied to that target. - foreach (['getenv', 'parse_str', 'stream_socket_client'] as $name) { - $compiler->validateNanoFunction($name); - } - self::addToAssertionCount(3); - } - - public function testFullRuntimeNanoEntryCallsGeneratedMainWithoutEval(): void - { - global $translator; - $previousTranslator = $translator ?? null; - $directory = sys_get_temp_dir() . '/typephp_nano_policy_' . bin2hex(random_bytes(6)); - mkdir($directory, 0777, true); - $source = $directory . '/main.php'; - file_put_contents($source, "enableFullRuntimeNanoPolicyForTest(); - $compiler->setBuildMode(\TypePhp\CompilerBase::BUILD_MODE_BIN); - $compiler->setTargetName('nano_policy_entry'); - $translator = $compiler; - $compiler->addFiles([$source]); - $compiler->prepareFile($source); - $compiler->convert([$source]); - $extension = file_get_contents($directory . '/build/extension-nano_policy_entry.cc'); - $entryHeader = basename($compiler->getDeclarationHeaderFile($source)); - - self::assertIsString($extension); - self::assertStringContainsString("#include <{$entryHeader}>", $extension); - self::assertStringContainsString('php_main();', $extension); - self::assertStringNotContainsString('php::eval(', $extension); - self::assertStringContainsString( - 'typephp_disable_nano_function("exec", 4);', - $extension, - ); - self::assertStringContainsString( - 'function->internal_function.handler = typephp_nano_disabled_function;', - $extension, - ); - self::assertStringContainsString( - 'static void ZEND_FASTCALL typephp_nano_disabled_function(INTERNAL_FUNCTION_PARAMETERS)', - $extension, - ); - self::assertStringNotContainsString('zend_disable_functions(', $extension); - self::assertStringContainsString('_SERVER.item("SCRIPT_FILENAME", true)', $extension); - } finally { - $translator = $previousTranslator; - self::removeDirectory($directory); - } - } - - private static function removeDirectory(string $directory): void - { - if (!is_dir($directory)) { - return; - } - foreach (array_diff(scandir($directory), ['.', '..']) as $entry) { - $path = $directory . DIRECTORY_SEPARATOR . $entry; - if (is_dir($path)) { - self::removeDirectory($path); - } else { - unlink($path); - } - } - rmdir($directory); } public function testKeepsFileStreamsAndFileHashesAvailable(): void diff --git a/phpunit/src/Transform/NanoSyntaxValidationVisitorTest.php b/phpunit/src/Transform/NanoSyntaxValidationVisitorTest.php index a647bb6e..132f414b 100644 --- a/phpunit/src/Transform/NanoSyntaxValidationVisitorTest.php +++ b/phpunit/src/Transform/NanoSyntaxValidationVisitorTest.php @@ -105,38 +105,4 @@ final class NanoSyntaxValidationVisitorTest extends TestCase self::assertCount(1, $traverser->traverse($nodes)); } - public function testFullRuntimeNanoPolicyKeepsGeneratorsButRejectsVmEntrySyntax(): void - { - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $accepted = $parser->parse( - 'addVisitor(new NameResolver(null, ['replaceNodes' => false])); - $traverser->addVisitor(new NanoSyntaxValidationVisitor( - static function (Node $node, string $message): never { - throw new RuntimeException($message . ':' . $node->getStartLine()); - }, - false, - )); - self::assertCount(1, $traverser->traverse($accepted)); - - foreach ([ - 'parse($source); - self::assertNotNull($nodes); - try { - $traverser->traverse($nodes); - self::fail("Nano policy accepted unsupported source: {$source}"); - } catch (RuntimeException) { - self::addToAssertionCount(1); - } - } - } } diff --git a/src/Backend/Msvc.php b/src/Backend/Msvc.php index 0cc85a75..fc52e4d9 100644 --- a/src/Backend/Msvc.php +++ b/src/Backend/Msvc.php @@ -39,7 +39,7 @@ class Msvc extends CompilerBackend // Generated code/templates can exceed ordinary COFF section limits. $cmd = ' /bigobj'; - $cmd .= ' /utf-8 /DZEND_WIN32 /DPHP_WIN32 /DZEND_DEBUG=0 /DENABLE_INTSAFE_SIGNED_FUNCTIONS'; + $cmd .= ' /utf-8 /Zc:preprocessor /DZEND_WIN32 /DPHP_WIN32 /DZEND_DEBUG=0 /DENABLE_INTSAFE_SIGNED_FUNCTIONS'; if (!empty($config['is_zts'])) { $cmd .= ' /DZTS'; @@ -92,14 +92,23 @@ class Msvc extends CompilerBackend $cmd .= ' /GL'; } + // Match -ffunction-sections/-fdata-sections used by the other Nano + // backends. /OPT:REF can discard an unused runtime function only when + // MSVC emitted it as an individual COMDAT. + if (!empty($config['section_gc'])) { + $cmd .= ' /Gy /Gw /Zc:inline'; + } + + // Keep every translation unit on the same dynamic CRT. In particular, + // source-composed Nano mixes PHP C sources with PHPX/TypePHP C++. + $cmd .= ' /MD'; + if ($includeCppOptions) { $cmd .= ' /EHsc'; if (!empty($config['cpp_std'])) { $cmd .= ' /std:' . $config['cpp_std']; } - $cmd .= ' /MD'; - if (!empty($config['cxxflags'])) { $cmd .= ' ' . $config['cxxflags']; } @@ -107,8 +116,13 @@ class Msvc extends CompilerBackend if (!empty($config['forced_include'])) { $cmd .= ' /FI' . escapeshellarg($config['forced_include']); } - } elseif (!empty($config['cflags'])) { - $cmd .= ' ' . $config['cflags']; + } else { + if (!empty($config['c_std'])) { + $cmd .= ' /std:' . $config['c_std']; + } + if (!empty($config['cflags'])) { + $cmd .= ' ' . $config['cflags']; + } } $cmd .= ' /nologo'; @@ -151,7 +165,7 @@ class Msvc extends CompilerBackend // Platform macro definitions. $cmd .= $this->buildCommonCompileFlags($options, false); - // Note: C files do not use C++-specific options such as /EHsc, /std:c++17, /MD. + // Note: C files do not use C++-specific options such as /EHsc or /std:c++17. return $cmd; } @@ -261,6 +275,10 @@ class Msvc extends CompilerBackend $cmd .= ' /LTCG'; } + if (!empty($config['section_gc'])) { + $cmd .= ' /OPT:REF /OPT:ICF'; + } + return $cmd; } diff --git a/src/Build/ComposerNativePackage.php b/src/Build/ComposerNativePackage.php index d9b5200c..00274c96 100644 --- a/src/Build/ComposerNativePackage.php +++ b/src/Build/ComposerNativePackage.php @@ -11,6 +11,7 @@ final readonly class ComposerNativePackage /** * @param list $includeDirs * @param list $sources + * @param list $defines * @param array $components */ private function __construct( @@ -22,6 +23,7 @@ final readonly class ComposerNativePackage public int $cxxStandard, public array $includeDirs, public array $sources, + public array $defines, public array $components, public ?string $extensionName, public ?string $extensionModuleEntry, @@ -29,7 +31,7 @@ final readonly class ComposerNativePackage } /** @return list */ - public static function discover(?string $compilerRoot = null): array + public static function discover(?string $compilerRoot = null, ?string $platformName = null): array { $packages = []; foreach (InstalledVersions::getInstalledPackages() as $package) { @@ -43,7 +45,7 @@ final readonly class ComposerNativePackage || !is_array($manifest['extra']['typephp-native'] ?? null)) { continue; } - $packages[] = self::load($package, $compilerRoot); + $packages[] = self::load($package, $compilerRoot, $platformName); } usort( @@ -57,7 +59,11 @@ final readonly class ComposerNativePackage return $packages; } - public static function load(string $package, ?string $compilerRoot = null): self + public static function load( + string $package, + ?string $compilerRoot = null, + ?string $platformName = null, + ): self { if ($package === 'swoole/php-ext-standard') { throw new RuntimeException( @@ -143,7 +149,28 @@ final readonly class ComposerNativePackage ); } + $platform = self::platformKey($platformName ?? PHP_OS_FAMILY); + $platforms = $native['platforms'] ?? []; + if (!is_array($platforms)) { + throw new RuntimeException("Invalid native platform metadata in `{$package}`"); + } + $platformNative = $platforms[$platform] ?? []; + if (!is_array($platformNative)) { + throw new RuntimeException("Invalid native metadata for platform `{$package}:{$platform}`"); + } + $sources = self::resolveEntries($root, $native['sources'] ?? null, false, $package); + if (($platformNative['sources'] ?? []) !== []) { + $sources = array_values(array_unique([ + ...$sources, + ...self::resolveEntries( + $root, + $platformNative['sources'], + false, + "{$package}:{$platform}", + ), + ])); + } foreach ($sources as $source) { $extension = strtolower(pathinfo($source, PATHINFO_EXTENSION)); if (!in_array($extension, ['c', 'cc', 'cpp', 'cxx'], true)) { @@ -212,6 +239,34 @@ final readonly class ComposerNativePackage ); } + $includeDirs = self::resolveEntries($root, $native['include-dirs'] ?? null, true, $package); + if (($platformNative['include-dirs'] ?? []) !== []) { + $includeDirs = array_values(array_unique([ + ...$includeDirs, + ...self::resolveEntries( + $root, + $platformNative['include-dirs'], + true, + "{$package}:{$platform}", + ), + ])); + } + $defines = self::stringList($native['defines'] ?? [], 'define', $package, 'runtime'); + array_push( + $defines, + ...self::stringList( + $platformNative['defines'] ?? [], + 'define', + $package, + $platform, + ), + ); + foreach ($defines as $define) { + if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*(?:=.*)?$/', $define) !== 1) { + throw new RuntimeException("Invalid native define in `{$package}:{$platform}`"); + } + } + return new self( $package, $root, @@ -219,14 +274,27 @@ final readonly class ComposerNativePackage $abi, $cStandard, $cxxStandard, - self::resolveEntries($root, $native['include-dirs'] ?? null, true, $package), + $includeDirs, $sources, + array_values(array_unique($defines)), $components, $extensionName, $extensionModuleEntry, ); } + private static function platformKey(string $platformName): string + { + return match (strtolower($platformName)) { + 'windows' => 'windows', + 'darwin', 'macos' => 'macos', + 'ios' => 'ios', + 'android' => 'android', + 'wasi', 'wasip2', 'wasm32-wasip2' => 'wasip2', + default => 'linux', + }; + } + /** @return list */ private static function stringList( mixed $values, diff --git a/src/Build/NanoBuildBackend.php b/src/Build/NanoBuildBackend.php index 0376d49b..2dde43db 100644 --- a/src/Build/NanoBuildBackend.php +++ b/src/Build/NanoBuildBackend.php @@ -2,22 +2,19 @@ namespace TypePhp\Build; -/** Selects the runtime build backend used by a --nano native application. */ +/** Selects the source-composed runtime backend used by a --nano application. */ final class NanoBuildBackend { - /** Windows native application linked through the PHP/PHPX import libraries. */ - public const WINDOWS_DLL = 'windows-dll'; - /** Composer package manifests whose C/C++ sources are compiled into the program. */ public const COMPOSER_SOURCES = 'composer-sources'; public static function forHost(string $platformName): string { - return $platformName === 'Windows' ? self::WINDOWS_DLL : self::COMPOSER_SOURCES; + return self::COMPOSER_SOURCES; } public static function composesRuntimeSources(string $platformName): bool { - return self::forHost($platformName) === self::COMPOSER_SOURCES; + return true; } } diff --git a/src/Build/NanoSourceComposer.php b/src/Build/NanoSourceComposer.php index c6e8cf9c..c74777d7 100644 --- a/src/Build/NanoSourceComposer.php +++ b/src/Build/NanoSourceComposer.php @@ -8,7 +8,10 @@ use TypePhp\Analysis\CompilationStatistics; /** Resolves the Composer-provided runtime sources used by a Nano build. */ final class NanoSourceComposer { - public function __construct(private readonly string $compilerRoot) + public function __construct( + private readonly string $compilerRoot, + private readonly string $platformName = PHP_OS_FAMILY, + ) { } @@ -29,8 +32,16 @@ final class NanoSourceComposer ?CompilationStatistics $statistics = null, ): array { - $runtime = ComposerNativePackage::load('swoole/php-nano', $this->compilerRoot); - $phpx = ComposerNativePackage::load('swoole/phpx', $this->compilerRoot); + $runtime = ComposerNativePackage::load( + 'swoole/php-nano', + $this->compilerRoot, + $this->platformName, + ); + $phpx = ComposerNativePackage::load( + 'swoole/phpx', + $this->compilerRoot, + $this->platformName, + ); if ($runtime->abi !== $phpx->abi) { throw new RuntimeException( "Native ABI mismatch: swoole/php-nano={$runtime->abi}, swoole/phpx={$phpx->abi}" @@ -41,7 +52,7 @@ final class NanoSourceComposer $runtime->name => $runtime, $phpx->name => $phpx, ]; - foreach (ComposerNativePackage::discover($this->compilerRoot) as $package) { + foreach (ComposerNativePackage::discover($this->compilerRoot, $this->platformName) as $package) { $packagesByName[$package->name] = $package; } $packages = array_values($packagesByName); @@ -69,6 +80,7 @@ final class NanoSourceComposer $includeDirs = []; $defines = []; foreach ($packages as $package) { + array_push($defines, ...$package->defines); if ($package->abi !== $runtime->abi) { throw new RuntimeException( "Native ABI mismatch: {$package->name}={$package->abi}, " diff --git a/src/Build/NativeBuildConfigurationTrait.php b/src/Build/NativeBuildConfigurationTrait.php index f6fc7585..37d7b6d9 100644 --- a/src/Build/NativeBuildConfigurationTrait.php +++ b/src/Build/NativeBuildConfigurationTrait.php @@ -236,7 +236,16 @@ trait NativeBuildConfigurationTrait protected function getLibraries(): array { if ($this->isNanoMode()) { - return []; + return $this->isWindows() + ? [ + 'advapi32.lib', + 'bcrypt.lib', + 'pathcch.lib', + 'shell32.lib', + 'user32.lib', + 'ws2_32.lib', + ] + : []; } $sdkDir = $this->getFullStaticSdkDir(); diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php index 103d45b5..96fb66cf 100644 --- a/src/Build/NativeCommandOptionsTrait.php +++ b/src/Build/NativeCommandOptionsTrait.php @@ -60,7 +60,9 @@ trait NativeCommandOptionsTrait 'sanitize' => $this->sanitize, 'march' => $this->march, 'target_platform' => $this->targetPlatform, - 'is_zts' => $this->isPhpZts, + // Source-composed Nano owns its runtime configuration and uses the + // same single-threaded ABI on every native platform. + 'is_zts' => $this->isNanoMode() ? false : $this->isPhpZts, 'build_mode' => $this->buildMode, 'enable_profiler' => $this->enableProfiler, 'prof_output' => $this->targetName . '.prof', @@ -187,10 +189,12 @@ trait NativeCommandOptionsTrait $ldflags = trim('-static -B ' . escapeshellarg($this->getFullStaticMuslDir()) . ' ' . $ldflags); } if ($this->isNanoMode()) { - $gcSections = ($this->isMacos() || $this->isIosTarget()) - ? '-Wl,-dead_strip' - : '-Wl,--gc-sections'; - $ldflags = trim($gcSections . ' ' . $ldflags); + if (!$this->isWindows()) { + $gcSections = ($this->isMacos() || $this->isIosTarget()) + ? '-Wl,-dead_strip' + : '-Wl,--gc-sections'; + $ldflags = trim($gcSections . ' ' . $ldflags); + } if ($this->isWasiTarget()) { $ldflags = trim( '-fwasm-exceptions -lsetjmp -lunwind ' . $ldflags, @@ -208,6 +212,7 @@ trait NativeCommandOptionsTrait 'build_mode' => $this->buildMode, 'sanitize' => $this->sanitize, 'lto' => $this->enableLto, + 'section_gc' => $this->isNanoMode(), 'target_platform' => $targetPlatform, 'response_file' => $this->getBuildDir() . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'link' . DIRECTORY_SEPARATOR diff --git a/src/Build/NativeDependencyAuditor.php b/src/Build/NativeDependencyAuditor.php index 71d0d0ee..ee3abf63 100644 --- a/src/Build/NativeDependencyAuditor.php +++ b/src/Build/NativeDependencyAuditor.php @@ -66,6 +66,35 @@ final class NativeDependencyAuditor } } + public function assertWindowsImports(string $dumpbinOutput): void + { + $runtimeDlls = []; + $forbidden = []; + foreach (preg_split('/\R/', $dumpbinOutput) ?: [] as $line) { + $value = trim($line); + if (preg_match('/^[A-Za-z0-9_.-]+\.dll$/i', $value) === 1 + && preg_match('/^php(?:x|\d.*)?\.dll$/i', $value) === 1) { + $runtimeDlls[strtolower($value)] = true; + } + if (preg_match('/(?:^|\s)([A-Za-z_][A-Za-z0-9_@?$]*)\s*$/', $value, $match) === 1 + && $this->isForbiddenSymbol($match[1], 'windows')) { + $forbidden[$match[1]] = true; + } + } + if ($runtimeDlls !== []) { + throw new RuntimeException( + 'PHP Nano Windows artifact imports a dynamic PHP runtime: ' + . implode(', ', array_keys($runtimeDlls)) + ); + } + if ($forbidden !== []) { + throw new RuntimeException( + 'PHP Nano Windows artifact imports forbidden host capabilities: ' + . implode(', ', array_keys($forbidden)) + ); + } + } + private function isForbiddenSymbol(string $symbol, string $target): bool { if ($target === 'wasip2') { diff --git a/src/Build/NativeSourceProjectBuilder.php b/src/Build/NativeSourceProjectBuilder.php index b65a00c8..73c552ee 100644 --- a/src/Build/NativeSourceProjectBuilder.php +++ b/src/Build/NativeSourceProjectBuilder.php @@ -25,12 +25,6 @@ final class NativeSourceProjectBuilder /** @return array{output: string, sourceCount: int, compiledCount: int} */ public function build(NativeSourceProjectConfig $project): array { - if ($project->target === 'native' && PHP_OS_FAMILY === 'Windows') { - throw new RuntimeException( - 'php-nano does not target Windows; use TypePHP --nano with the full PHP/PHPX DLL runtime' - ); - } - $this->writeProgress("Preparing Nano {$project->target} build: {$project->name}"); $compiler = $this->resolveExecutable($project->compiler); @@ -58,7 +52,10 @@ final class NativeSourceProjectBuilder $generatedIncludeDir = $generatedDir . DIRECTORY_SEPARATOR . 'include'; } - $composition = (new NanoSourceComposer($this->compilerRuntime->installationRoot))->compose( + $composition = (new NanoSourceComposer( + $this->compilerRuntime->installationRoot, + PHP_OS_FAMILY, + ))->compose( $project->buildDir, $this->projectSymbolName($project), $project->phpSources !== [], diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 870f04cd..db84d361 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -335,25 +335,6 @@ abstract class CompilerBase implements PropertyAccessContext 'syslog', ]; - /** Calls forbidden by Nano policy even when the full Windows PHP DLL is used. */ - private const array NANO_POLICY_UNSUPPORTED_FUNCTIONS = [ - 'exec', - 'passthru', - 'pcntl_exec', - 'popen', - 'proc_close', - 'proc_get_status', - 'proc_nice', - 'proc_open', - 'proc_terminate', - 'shell_exec', - 'system', - ]; - - private const array NANO_POLICY_UNSUPPORTED_FUNCTION_PREFIXES = [ - 'proc_', - ]; - private const array NANO_UNSUPPORTED_FUNCTION_PREFIXES = [ 'pcntl_', 'posix_', @@ -964,21 +945,6 @@ abstract class CompilerBase implements PropertyAccessContext } $name = strtolower(ltrim($name, '\\')); - if (in_array($name, self::NANO_POLICY_UNSUPPORTED_FUNCTIONS, true)) { - $this->fatalError($expr, "Function `{$name}` is not supported in nano mode"); - } - foreach (self::NANO_POLICY_UNSUPPORTED_FUNCTION_PREFIXES as $prefix) { - if (str_starts_with($name, $prefix)) { - $this->fatalError($expr, "Function `{$name}` is not supported in nano mode"); - } - } - - // Windows Nano uses the complete PHP/PHPX DLL set. Only the common - // policy above applies; php-nano's smaller host surface is Unix/WASI. - if (!$this->isNanoMode()) { - return; - } - if (in_array($name, self::NANO_UNSUPPORTED_FUNCTIONS, true)) { $this->fatalError($expr, "Function `{$name}` is not supported in nano mode"); } @@ -989,11 +955,6 @@ abstract class CompilerBase implements PropertyAccessContext } } - protected function getNanoPolicyDisabledFunctionList(): string - { - return implode(',', self::NANO_POLICY_UNSUPPORTED_FUNCTIONS); - } - public function isBuildModeBin(): bool { return $this->buildMode === self::BUILD_MODE_BIN; diff --git a/src/Metadata/Constants.php b/src/Metadata/Constants.php index c5a6adf8..416c896c 100644 --- a/src/Metadata/Constants.php +++ b/src/Metadata/Constants.php @@ -131,7 +131,7 @@ class Constants public const array COMPILER_OPTIONS = [ 'nano' => [ 'longPrefix' => 'nano', - 'description' => 'Enable VM-free Nano policy (php-nano runtime outside Windows)', + 'description' => 'Enable the VM-free, source-composed php-nano runtime', 'required' => false, 'noValue' => true, ], diff --git a/src/Preprocessor.php b/src/Preprocessor.php index c35f5a7e..4da349f3 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -483,7 +483,6 @@ abstract class Preprocessor extends CompilerBase if ($this->isNanoPolicyMode()) { $traverser->addVisitor(new NanoSyntaxValidationVisitor( fn (Node $node, string $message) => $this->fatalError($node, $message), - $this->isNanoMode(), )); } $traverser->addVisitor(new VoidCastValidationVisitor( diff --git a/src/Transform/NanoSyntaxValidationVisitor.php b/src/Transform/NanoSyntaxValidationVisitor.php index 5435e5cb..a78a0d5f 100644 --- a/src/Transform/NanoSyntaxValidationVisitor.php +++ b/src/Transform/NanoSyntaxValidationVisitor.php @@ -18,13 +18,12 @@ final class NanoSyntaxValidationVisitor extends NodeVisitorAbstract /** @param callable(Node, string): never $fatal */ public function __construct( private readonly mixed $fatal, - private readonly bool $phpNanoRuntime = true, ) { } public function enterNode(Node $node): ?Node { - if ($this->phpNanoRuntime && $node instanceof Node\Name) { + if ($node instanceof Node\Name) { $resolved = $node->getAttribute('resolvedName'); $className = $resolved instanceof Node\Name ? $resolved->toString() @@ -60,8 +59,7 @@ final class NanoSyntaxValidationVisitor extends NodeVisitorAbstract ($this->fatal)($node, 'Anonymous classes are not supported in nano mode'); } - if ($this->phpNanoRuntime - && ($node instanceof Node\Expr\Yield_ || $node instanceof Node\Expr\YieldFrom)) { + if ($node instanceof Node\Expr\Yield_ || $node instanceof Node\Expr\YieldFrom) { ($this->fatal)( $node, 'Fiber and Generator are not supported in nano mode because C++17 has no standard stack-switching API', diff --git a/src/Translator.php b/src/Translator.php index 043fd355..d8167356 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -449,7 +449,7 @@ class Translator extends Preprocessor ['--march ', 'Target CPU instruction set (for example native or armv8-a)'], ['--target-platform ', 'Cross-compilation target triple'], ['--wasm[=browser|component]', 'Build WASI component (default) or browser output'], - ['--nano', 'Build a Nano application (Windows uses the PHP/PHPX DLL backend)'], + ['--nano', 'Build a source-composed Nano application'], ['--full-static', 'Link fully statically against the bundled SDK'], ['--lto', 'Enable Link Time Optimization (-flto)'], ['--no-literal-strings', 'Disable literal string optimization'], @@ -493,14 +493,16 @@ class Translator extends Preprocessor $this->downloadProxy = $proxy; } - // The Nano syntax policy is platform-independent. Windows produces a - // native application through the PHP/PHPX DLL backend; other targets - // compose the php-nano runtime sources into the artifact. + // Every native platform composes the same php-nano and PHPX runtime + // sources directly into the output artifact. if ($this->climate->arguments->defined('nano')) { $this->nanoPolicyMode = true; if (NanoBuildBackend::composesRuntimeSources($this->getPlatform()->getName())) { $this->nanoMode = true; $this->noLiteralStrings = true; + // Nano is source-composed as NTS on every native host. Do not + // fold the build-host PHP_ZTS value into generated programs. + $this->internalConstants['PHP_ZTS'] = false; } } @@ -660,10 +662,7 @@ class Translator extends Preprocessor $this->error('--nano requires the C++17 language standard'); } if ($this->fullStatic) { - $message = $this->isNanoMode() - ? '--nano already composes its runtime sources; --full-static is not applicable' - : '--nano on Windows uses the PHP/PHPX DLL backend; --full-static is not supported'; - $this->error($message); + $this->error('--nano already composes its runtime sources; --full-static is not applicable'); } // Nano applications may consume target-owned static libraries. // The final executable dependency audit remains the authority on @@ -1838,30 +1837,6 @@ CODE; // module_clean end $moduleName = $this->getModuleName(); - $installNanoPolicyHandlers = $this->isNanoPolicyMode() - && !$this->isNanoMode() - && $this->isBuildModeBin() - && $this->hasSapi('embed'); - if ($installNanoPolicyHandlers) { - // Windows Nano uses the full PHP runtime. Keep forbidden process - // functions in Zend's persistent table so shutdown boundaries stay - // intact, but replace their handlers before generated code runs. - // This path is binary/embed-only and executes once per process. - $code .= <<<'CODE' -static void ZEND_FASTCALL typephp_nano_disabled_function(INTERNAL_FUNCTION_PARAMETERS) { - const zend_string *name = EX(func)->common.function_name; - zend_throw_error(nullptr, "Function `%s` is not supported in nano mode", name ? ZSTR_VAL(name) : "unknown"); -} - -static void typephp_disable_nano_function(const char *name, size_t name_length) { - auto *function = static_cast(zend_hash_str_find_ptr(EG(function_table), name, name_length)); - if (function != nullptr && function->type == ZEND_INTERNAL_FUNCTION) { - function->internal_function.handler = typephp_nano_disabled_function; - } -} - -CODE; - } // rinit begin $code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL; $code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL; @@ -1878,18 +1853,6 @@ CODE; $code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL; $code .= '}' . PHP_EOL; $code .= 'php::request_init();' . PHP_EOL; - if ($installNanoPolicyHandlers) { - // The full Windows runtime still contains standard/process modules. - // Block command functions after every module has - // started so variable functions and call_user_func cannot bypass - // the compile-time named-call check. Replacing handlers preserves - // Zend's persistent function-table layout for embed shutdown. - foreach (explode(',', $this->getNanoPolicyDisabledFunctionList()) as $functionName) { - $functionArg = $this->genCharPtr($functionName, true); - $code .= 'typephp_disable_nano_function(' . $functionArg . ', ' - . strlen($functionName) . ');' . PHP_EOL; - } - } $code .= 'module_init();' . PHP_EOL; if ($this->isBuildModeBin() && $this->hasSapi('embed') && !$this->isNanoMode()) { @@ -1897,42 +1860,28 @@ CODE; $code .= 'if (strcmp(sapi_module.name, "embed") == 0) {' . PHP_EOL; } $entryFunction = $this->symbols->function(self::ENTRY_FUNCTION); - if ($this->isNanoPolicyMode()) { - // Windows keeps the complete PHP/PHPX DLL runtime, but a Nano - // executable still enters generated code without ZendVM eval. - $code .= $this->registerServerEnvironment($entryFunction->sourceFile); - $entryCall = count($entryFunction->argInfoList) === 2 - ? 'php_main(php::global("argc").toInt(), php::global("argv").toArray());' - : 'php_main();'; - $code .= 'try {' . PHP_EOL; - $code .= $this->getIndent(2) . $entryCall . PHP_EOL; - $code .= '} catch (zend_object *) {' . PHP_EOL; - $code .= $this->getIndent(2) . 'return FAILURE;' . PHP_EOL; - $code .= '}' . PHP_EOL; + // FunctionDef::sourceFile comes from loadFile()'s realpath(), so the + // CLI script fields always identify main()'s canonical absolute file. + $entryFile = $entryFunction->sourceFile; + $entryFileArg = $this->genCharPtr($entryFile, true); + $entryLineOffset = max(0, $entryFunction->startLine - 1); + if (count($entryFunction->argInfoList) == 2) { + $entryScript = 'global $argc, $argv; main($argc, $argv);'; } else { - // FunctionDef::sourceFile comes from loadFile()'s realpath(), so the - // CLI script fields always identify main()'s canonical absolute file. - $entryFile = $entryFunction->sourceFile; - $entryFileArg = $this->genCharPtr($entryFile, true); - $entryLineOffset = max(0, $entryFunction->startLine - 1); - if (count($entryFunction->argInfoList) == 2) { - $entryScript = 'global $argc, $argv; main($argc, $argv);'; - } else { - $entryScript = 'main();'; - } - - $entryScriptArg = $this->genCharPtr($entryScript, true); - if ($entryLineOffset > 0) { - // entryLineOffset is main()'s source start line minus one. The - // generated std::string(N, '\n') supplies N padding newlines at - // runtime, so the eval() entry call is reported on main()'s - // original PHP source line. Constructing the padding at runtime - // avoids embedding hundreds of escaped newlines in the C++ file. - $entryScriptArg = 'std::string(' . $entryLineOffset . ", '\\n') + " . $entryScriptArg; - } + $entryScript = 'main();'; + } - $code .= 'php::eval(' . $entryScriptArg . ', ' . $entryFileArg . ');' . PHP_EOL; + $entryScriptArg = $this->genCharPtr($entryScript, true); + if ($entryLineOffset > 0) { + // entryLineOffset is main()'s source start line minus one. The + // generated std::string(N, '\n') supplies N padding newlines at + // runtime, so the eval() entry call is reported on main()'s + // original PHP source line. Constructing the padding at runtime + // avoids embedding hundreds of escaped newlines in the C++ file. + $entryScriptArg = 'std::string(' . $entryLineOffset . ", '\\n') + " . $entryScriptArg; } + + $code .= 'php::eval(' . $entryScriptArg . ', ' . $entryFileArg . ');' . PHP_EOL; if ($this->isSapiBuild()) { $code .= '}' . PHP_EOL; } @@ -2604,7 +2553,10 @@ CODE; /** @param list $generatedSources @return list */ private function composeNanoRuntimeSources(array $generatedSources): array { - $composition = (new NanoSourceComposer($this->compilerRuntime->installationRoot))->compose( + $composition = (new NanoSourceComposer( + $this->compilerRuntime->installationRoot, + $this->getPlatform()->getName(), + ))->compose( $this->getBuildDir(), $this->targetName, true, @@ -3060,6 +3012,12 @@ CODE; ); return; } + if ($this->isWindows()) { + $auditor->assertWindowsImports( + $this->captureNativeCommand(['dumpbin', '/imports', $targetFile]), + ); + return; + } /* Native objects may deliberately provide an OS ABI to one another * (for example a freestanding syscall shim). Only unresolved imports * in the final ELF cross the host-capability boundary. */ @@ -3583,25 +3541,6 @@ CODE; $declarationHeaders[] = $header; } } - // Nano policy mode (Windows, the WINDOWS_DLL backend) emits a direct - // php_main() call inside RINIT instead of relying on ZendVM eval, so - // the entry function's declaration header — which carries the - // php_main() prototype — must be visible to this translation unit. - // True Nano mode keeps php_main() encapsulated inside the separate - // nano-entry translation unit, so only the policy path needs this. - if ($this->isNanoPolicyMode() - && !$this->isNanoMode() - && $this->isBuildModeBin() - && $this->hasSapi('embed') - && $this->hasFunction(self::ENTRY_FUNCTION) - ) { - $entryHeader = $this->declarationHeaderFiles[ - $this->getFunction(self::ENTRY_FUNCTION)->sourceFile - ] ?? null; - if ($entryHeader !== null && !in_array($entryHeader, $declarationHeaders, true)) { - $declarationHeaders[] = $entryHeader; - } - } } return $this->renderIncludeHeaderFiles([ @@ -4844,7 +4783,6 @@ CODE; if ($this->isNanoPolicyMode()) { $traverser->addVisitor(new NanoSyntaxValidationVisitor( fn (Node $node, string $message) => $this->fatalError($node, $message), - $this->isNanoMode(), )); } $traverser->addVisitor(new VoidCastValidationVisitor(