refactor(nano): unify Windows and Unix platforms for source-composed runtime

- Remove Windows-specific DLL backend and unify all platforms to source-composition model
- Add MSVC compiler support for section garbage collection with /Gy, /Gw, and /OPT flags
- Introduce platform-specific configuration loading for composer native packages
- Update nano capability policy to consistently reject external commands across all platforms
- Replace Windows DLL import checks with comprehensive symbol auditing
- Modify build configuration to handle platform-specific libraries and compilation flags
- Update documentation to reflect unified source-composition approach on Windows
- Remove deprecated nano policy modes and simplify syntax validation visitor
- Add proper C/C++ standard handling and preprocessing flags for MSVC
- Implement consistent section garbage collection for optimized linking on all platforms
master
韩天峰 1 week ago
parent 9671641629
commit 536cf5a857
  1. 48
      .github/workflows/windows-build.yml
  2. 19
      README-CN.md
  3. 27
      README.md
  4. 2
      composer.json
  5. 13
      phpunit/src/Backend/BackendOptionsTest.php
  6. 23
      phpunit/src/Build/ComposerNativePackageTest.php
  7. 13
      phpunit/src/Build/NanoBuildBackendTest.php
  8. 26
      phpunit/src/Build/NativeDependencyAuditorTest.php
  9. 4
      phpunit/src/CompilerBaseApiTest.php
  10. 75
      phpunit/src/NanoCapabilityPolicyTest.php
  11. 34
      phpunit/src/Transform/NanoSyntaxValidationVisitorTest.php
  12. 30
      src/Backend/Msvc.php
  13. 76
      src/Build/ComposerNativePackage.php
  14. 9
      src/Build/NanoBuildBackend.php
  15. 20
      src/Build/NanoSourceComposer.php
  16. 11
      src/Build/NativeBuildConfigurationTrait.php
  17. 15
      src/Build/NativeCommandOptionsTrait.php
  18. 29
      src/Build/NativeDependencyAuditor.php
  19. 11
      src/Build/NativeSourceProjectBuilder.php
  20. 39
      src/CompilerBase.php
  21. 2
      src/Metadata/Constants.php
  22. 1
      src/Preprocessor.php
  23. 6
      src/Transform/NanoSyntaxValidationVisitor.php
  24. 134
      src/Translator.php

@ -386,7 +386,7 @@ jobs:
throw "Windows smoke executable failed with exit code $($process.ExitCode)" throw "Windows smoke executable failed with exit code $($process.ExitCode)"
} }
if ($stdout.Trim() -ne 'windows-smoke-ok:zts') { 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 - name: Run Windows Nano smoke test
@ -397,7 +397,8 @@ jobs:
$env:PHPRC = Join-Path $env:PHP_HOME 'php.ini' $env:PHPRC = Join-Path $env:PHP_HOME 'php.ini'
$env:PATH = "$env:PHPX_HOME\build;$env:PATH" $env:PATH = "$env:PHPX_HOME\build;$env:PATH"
$nanoExe = Join-Path '${{ github.workspace }}' 'tests\windows\smoke\windows_nano_smoke.exe' $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) { if ($LASTEXITCODE -ne 0) {
throw "Windows Nano smoke project compilation failed with exit code $LASTEXITCODE" 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" 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 = [Diagnostics.ProcessStartInfo]::new()
$processInfo.FileName = $nanoExe $processInfo.FileName = $nanoExe
$processInfo.ArgumentList.Add('zts') $processInfo.ArgumentList.Add('nts')
$processInfo.UseShellExecute = $false $processInfo.UseShellExecute = $false
$processInfo.RedirectStandardOutput = $true $processInfo.RedirectStandardOutput = $true
$processInfo.RedirectStandardError = $true $processInfo.RedirectStandardError = $true
$processInfo.Environment['PATH'] = "$env:SystemRoot\System32;$env:SystemRoot"
$processInfo.Environment.Remove('PHPRC')
$process = [Diagnostics.Process]::new() $process = [Diagnostics.Process]::new()
$process.StartInfo = $processInfo $process.StartInfo = $processInfo
@ -429,26 +440,32 @@ jobs:
if ($process.ExitCode -ne 0) { if ($process.ExitCode -ne 0) {
throw "Windows Nano smoke executable failed with exit code $($process.ExitCode)" throw "Windows Nano smoke executable failed with exit code $($process.ExitCode)"
} }
if ($stdout.Trim() -ne 'windows-smoke-ok:zts') { if ($stdout.Trim() -ne 'windows-smoke-ok:nts') {
throw "Unexpected Windows Nano smoke output: $stdout" throw "Unexpected Windows Nano smoke output: $stdout"
} }
$policyInfo = [Diagnostics.ProcessStartInfo]::new() $policyInfo = [Diagnostics.ProcessStartInfo]::new()
$policyInfo.FileName = $nanoExe $policyInfo.FileName = $nanoExe
$policyInfo.ArgumentList.Add('zts') $policyInfo.ArgumentList.Add('nts')
$policyInfo.ArgumentList.Add('nano') $policyInfo.ArgumentList.Add('nano')
$policyInfo.UseShellExecute = $false $policyInfo.UseShellExecute = $false
$policyInfo.RedirectStandardOutput = $true $policyInfo.RedirectStandardOutput = $true
$policyInfo.RedirectStandardError = $true $policyInfo.RedirectStandardError = $true
$policyInfo.Environment['PATH'] = "$env:SystemRoot\System32;$env:SystemRoot"
$policyInfo.Environment.Remove('PHPRC')
$policyProcess = [Diagnostics.Process]::new() $policyProcess = [Diagnostics.Process]::new()
$policyProcess.StartInfo = $policyInfo $policyProcess.StartInfo = $policyInfo
if (-not $policyProcess.Start()) { if (-not $policyProcess.Start()) {
throw 'Unable to start the Windows Nano policy probe' 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() $policyStdout = $policyProcess.StandardOutput.ReadToEnd()
$policyStderr = $policyProcess.StandardError.ReadToEnd() $policyStderr = $policyProcess.StandardError.ReadToEnd()
$policyProcess.WaitForExit()
Write-Host "Windows Nano policy stdout: $policyStdout" Write-Host "Windows Nano policy stdout: $policyStdout"
Write-Host "Windows Nano policy stderr: $policyStderr" Write-Host "Windows Nano policy stderr: $policyStderr"
@ -457,15 +474,16 @@ jobs:
throw 'Windows Nano policy probe unexpectedly succeeded' throw 'Windows Nano policy probe unexpectedly succeeded'
} }
$policyOutput = "$policyStdout`n$policyStderr" $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" throw "Windows Nano policy probe returned an unexpected error: $policyOutput"
} }
# A GUI-subsystem Nano application uses the same generated entry and # A GUI-subsystem Nano application uses the same source-composed
# DLL backend, but must also link without a console window. # runtime, but must also link without a console window.
$nanoGuiExe = Join-Path '${{ github.workspace }}' 'tests\windows\smoke\windows_nano_gui_smoke.exe' $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' $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) { if ($LASTEXITCODE -ne 0) {
throw "Windows Nano GUI project compilation failed with exit code $LASTEXITCODE" throw "Windows Nano GUI project compilation failed with exit code $LASTEXITCODE"
} }
@ -473,6 +491,14 @@ jobs:
if (-not (Test-Path $nanoGuiExe)) { if (-not (Test-Path $nanoGuiExe)) {
throw "Windows Nano GUI executable was not generated: $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 - name: Package tested Windows compiler
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')

@ -243,20 +243,19 @@ string(16) "Linux ..."
默认可执行文件生成在执行 `tpc` 时的当前目录;普通模式与 Nano 模式共用 默认可执行文件生成在执行 `tpc` 时的当前目录;普通模式与 Nano 模式共用
`build` 目录保存生成代码、目标文件等中间产物。可使用 `-o` 显式修改输出路径。 `build` 目录保存生成代码、目标文件等中间产物。可使用 `-o` 显式修改输出路径。
PHP 与 Composer 仅用于编译期。在 Linux、macOS、iOS、Android 上,生成的程序 PHP 与 Composer 仅用于编译期。在 Windows、Linux、macOS、iOS、Android 上,
使用静态选定的 Nano 运行时及仅文件模式的 stream。Native Nano 可使用 C11、 生成程序都会把选中的 PHP Nano 与 PHPX 源码直接编译进最终 exe 或库,不导入
C++17 与 POSIX.1-2008,但依然不提供 socket、DNS、网络、远程 stream、动态 PHP `php.dll`、`phpx.dll`,也不依赖宿主 `libphp`/`libphpx`。运行时使用仅文件模式的
加载及进程执行能力。WASI 是更小的能力子集,直接调用目标不支持的 API 会在 stream,依然不提供 socket、DNS、网络、远程 stream、动态 PHP 加载及进程执行
编译期报错。 能力。WASI 是更小的能力子集,直接调用目标不支持的 API 会在编译期报错。
所有平台的 `--nano` 都会拒绝 `eval`、`include`、`include_once`、`require`、 所有平台的 `--nano` 都会拒绝 `eval`、`include`、`include_once`、`require`、
`require_once` 等 VM 入口以及匿名类。 `require_once` 等 VM 入口以及匿名类。
Windows 通过独立构建后端支持 `mode: bin` 和 `mode: lib` 的 Nano 原生应用:它仍走 Windows 的 `mode: bin` 和 `mode: lib` 与 Linux、macOS 使用相同的源码组合契约:
宿主机编译、链接流程,通过 import library 连接 `php.dll` 与 `phpx.dll`。Windows 不加载 MSVC 将 C11 PHP Nano、C++17 PHPX 与生成的 TypePHP 源码连接为同一个 PE 产物;
`swoole/php-nano`、`swoole/phpx` 的源码清单,也不会把它们的 C/C++ 源文件加入 仅允许保留正常的 Windows 系统库和编译器运行库依赖。外部命令 API 与反引号语法
项目 `sources`。外部命令 API 与反引号语法依然会被拒绝;请求启动时还会从 Zend 在所有平台均由相同的 Nano 能力策略拒绝。
函数表移除这些命令函数,避免变量函数或回调形式绕过编译期检查。
除运行时 sources、头文件目录、编译宏和链接输入外,Nano 与普通模式共用同一套 除运行时 sources、头文件目录、编译宏和链接输入外,Nano 与普通模式共用同一套
命令行参数解析、TypePHP 代码生成、并行任务调度、编译进度条、输出路径规则以及 命令行参数解析、TypePHP 代码生成、并行任务调度、编译进度条、输出路径规则以及

@ -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, Normal and Nano builds share the `build` directory for generated code, objects,
and other intermediate files. Use `-o` to select a different output path. 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, PHP and Composer remain build-time tools. On Windows, Linux, macOS, iOS, and
the generated program uses the statically selected Nano runtime and its Android, the generated program compiles the selected PHP Nano and PHPX sources
file-only stream layer. Native Nano may use C11, C++17, and POSIX.1-2008, but directly into the final executable or library. It does not import `php.dll`,
socket/DNS/network, remote streams, dynamic PHP loading, and process execution `phpx.dll`, or a host `libphp`/`libphpx`. The runtime retains its file-only
remain unavailable. WASI is a smaller subset; direct calls to APIs missing from stream layer; socket/DNS/network, remote streams, dynamic PHP loading, and
that target are compile-time errors. 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`, On every platform, `--nano` rejects the VM entry paths `eval`, `include`,
`include_once`, `require`, and `require_once`, as well as anonymous classes. `include_once`, `require`, and `require_once`, as well as anonymous classes.
Windows supports Nano native applications in `mode: bin` and `mode: lib` through Windows supports Nano native applications in `mode: bin` and `mode: lib` with
a different build backend: it keeps the existing host compile/link pipeline and the same source-composition contract as Linux and macOS. MSVC compiles the C11
connects to `php.dll` and `phpx.dll` through their import libraries. It does not PHP Nano sources, the C++17 PHPX sources, and generated TypePHP sources into one
load the `swoole/php-nano` or PE artifact; only Windows system and compiler-runtime DLLs may remain as normal
`swoole/phpx` source manifests, nor append their C/C++ files to project `sources`. platform dependencies. External-command APIs and backtick syntax are rejected
External-command APIs and backtick syntax are still rejected. Those command by the same Nano capability policy on every platform.
functions are also removed from the Zend function table at request startup, so
indirect variable/callback calls cannot bypass the policy.
Except for runtime sources, include directories, compile definitions, and link Except for runtime sources, include directories, compile definitions, and link
inputs, Nano and normal mode share command-line parsing, TypePHP code generation, inputs, Nano and normal mode share command-line parsing, TypePHP code generation,

@ -29,7 +29,7 @@
"phpunit/phpunit": "^10.4", "phpunit/phpunit": "^10.4",
"friendsofphp/php-cs-fixer": "^3.40", "friendsofphp/php-cs-fixer": "^3.40",
"phpstan/phpstan": "^2.2", "phpstan/phpstan": "^2.2",
"swoole/php-nano": "^1.0.2" "swoole/php-nano": "^1.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

@ -37,6 +37,19 @@ class BackendOptionsTest extends TestCase
$this->assertStringContainsString('/nologo', $options); $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 模式 * 测试 MSVC 编译选项 - ZTS 模式
*/ */

@ -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 private function installFixture(bool $requireRuntime): void
{ {
$manifest = [ $manifest = [

@ -7,22 +7,17 @@ use TypePhp\Build\NanoBuildBackend;
final class NanoBuildBackendTest extends TestCase final class NanoBuildBackendTest extends TestCase
{ {
public function testWindowsKeepsTheHostDllBuildBackend(): void /** @dataProvider nativeHosts */
{ public function testEveryNativeHostComposesComposerRuntimeSources(string $osFamily): void
self::assertSame(NanoBuildBackend::WINDOWS_DLL, NanoBuildBackend::forHost('Windows'));
self::assertFalse(NanoBuildBackend::composesRuntimeSources('Windows'));
}
/** @dataProvider nonWindowsHosts */
public function testNonWindowsHostsComposeComposerRuntimeSources(string $osFamily): void
{ {
self::assertSame(NanoBuildBackend::COMPOSER_SOURCES, NanoBuildBackend::forHost($osFamily)); self::assertSame(NanoBuildBackend::COMPOSER_SOURCES, NanoBuildBackend::forHost($osFamily));
self::assertTrue(NanoBuildBackend::composesRuntimeSources($osFamily)); self::assertTrue(NanoBuildBackend::composesRuntimeSources($osFamily));
} }
public static function nonWindowsHosts(): array public static function nativeHosts(): array
{ {
return [ return [
['Windows'],
['Linux'], ['Linux'],
['Darwin'], ['Darwin'],
['BSD'], ['BSD'],

@ -146,4 +146,30 @@ final class NativeDependencyAuditorTest extends TestCase
); );
self::addToAssertionCount(1); 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",
);
}
} }

@ -1348,7 +1348,7 @@ YAML);
$this->invokeMethod('applyCommandLineArguments'); $this->invokeMethod('applyCommandLineArguments');
} }
public function testWindowsNanoUsesNativeDllBackendAndRejectsExtensionMode(): void public function testWindowsNanoComposesRuntimeSourcesAndRejectsExtensionMode(): void
{ {
global $argv; global $argv;
$argv = ['compiler.php', '--nano']; $argv = ['compiler.php', '--nano'];
@ -1364,7 +1364,7 @@ YAML);
$apply->invoke($compiler); $apply->invoke($compiler);
self::assertTrue($compiler->isNanoPolicyMode()); self::assertTrue($compiler->isNanoPolicyMode());
self::assertFalse($compiler->isNanoMode()); self::assertTrue($compiler->isNanoMode());
$buildMode = $reflection->getProperty('buildMode'); $buildMode = $reflection->getProperty('buildMode');
$buildMode->setAccessible(true); $buildMode->setAccessible(true);

@ -23,7 +23,7 @@ final class NanoCapabilityPolicyCompiler extends CompilerTest
$this->file = 'nano-policy.php'; $this->file = 'nano-policy.php';
} }
public function enableFullRuntimeNanoPolicyForTest(): void public function enableNanoPolicyWithoutRuntimeForTest(): void
{ {
$this->nanoMode = false; $this->nanoMode = false;
$this->nanoPolicyMode = true; $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 = 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 { try {
$compiler->validateNanoFunction($name); $compiler->validateNanoFunction($name);
self::fail("{$name} was accepted"); 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, "<?php\nfunction main(): void {}\n");
try {
$compiler = new NanoCapabilityPolicyCompiler($directory);
$compiler->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 public function testKeepsFileStreamsAndFileHashesAvailable(): void

@ -105,38 +105,4 @@ final class NanoSyntaxValidationVisitorTest extends TestCase
self::assertCount(1, $traverser->traverse($nodes)); self::assertCount(1, $traverser->traverse($nodes));
} }
public function testFullRuntimeNanoPolicyKeepsGeneratorsButRejectsVmEntrySyntax(): void
{
$parser = (new ParserFactory())->createForNewestSupportedVersion();
$accepted = $parser->parse(
'<?php function values(): Generator { yield 1; }',
);
self::assertNotNull($accepted);
$traverser = new NodeTraverser();
$traverser->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 ([
'<?php eval("return 1;");',
'<?php require "a.php";',
'<?php `uname`;',
'<?php $value = new class {};',
] as $source) {
$nodes = $parser->parse($source);
self::assertNotNull($nodes);
try {
$traverser->traverse($nodes);
self::fail("Nano policy accepted unsupported source: {$source}");
} catch (RuntimeException) {
self::addToAssertionCount(1);
}
}
}
} }

@ -39,7 +39,7 @@ class Msvc extends CompilerBackend
// Generated code/templates can exceed ordinary COFF section limits. // Generated code/templates can exceed ordinary COFF section limits.
$cmd = ' /bigobj'; $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'])) { if (!empty($config['is_zts'])) {
$cmd .= ' /DZTS'; $cmd .= ' /DZTS';
@ -92,14 +92,23 @@ class Msvc extends CompilerBackend
$cmd .= ' /GL'; $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) { if ($includeCppOptions) {
$cmd .= ' /EHsc'; $cmd .= ' /EHsc';
if (!empty($config['cpp_std'])) { if (!empty($config['cpp_std'])) {
$cmd .= ' /std:' . $config['cpp_std']; $cmd .= ' /std:' . $config['cpp_std'];
} }
$cmd .= ' /MD';
if (!empty($config['cxxflags'])) { if (!empty($config['cxxflags'])) {
$cmd .= ' ' . $config['cxxflags']; $cmd .= ' ' . $config['cxxflags'];
} }
@ -107,8 +116,13 @@ class Msvc extends CompilerBackend
if (!empty($config['forced_include'])) { if (!empty($config['forced_include'])) {
$cmd .= ' /FI' . escapeshellarg($config['forced_include']); $cmd .= ' /FI' . escapeshellarg($config['forced_include']);
} }
} elseif (!empty($config['cflags'])) { } else {
$cmd .= ' ' . $config['cflags']; if (!empty($config['c_std'])) {
$cmd .= ' /std:' . $config['c_std'];
}
if (!empty($config['cflags'])) {
$cmd .= ' ' . $config['cflags'];
}
} }
$cmd .= ' /nologo'; $cmd .= ' /nologo';
@ -151,7 +165,7 @@ class Msvc extends CompilerBackend
// Platform macro definitions. // Platform macro definitions.
$cmd .= $this->buildCommonCompileFlags($options, false); $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; return $cmd;
} }
@ -261,6 +275,10 @@ class Msvc extends CompilerBackend
$cmd .= ' /LTCG'; $cmd .= ' /LTCG';
} }
if (!empty($config['section_gc'])) {
$cmd .= ' /OPT:REF /OPT:ICF';
}
return $cmd; return $cmd;
} }

@ -11,6 +11,7 @@ final readonly class ComposerNativePackage
/** /**
* @param list<string> $includeDirs * @param list<string> $includeDirs
* @param list<string> $sources * @param list<string> $sources
* @param list<string> $defines
* @param array<string, ComposerNativeComponent> $components * @param array<string, ComposerNativeComponent> $components
*/ */
private function __construct( private function __construct(
@ -22,6 +23,7 @@ final readonly class ComposerNativePackage
public int $cxxStandard, public int $cxxStandard,
public array $includeDirs, public array $includeDirs,
public array $sources, public array $sources,
public array $defines,
public array $components, public array $components,
public ?string $extensionName, public ?string $extensionName,
public ?string $extensionModuleEntry, public ?string $extensionModuleEntry,
@ -29,7 +31,7 @@ final readonly class ComposerNativePackage
} }
/** @return list<self> */ /** @return list<self> */
public static function discover(?string $compilerRoot = null): array public static function discover(?string $compilerRoot = null, ?string $platformName = null): array
{ {
$packages = []; $packages = [];
foreach (InstalledVersions::getInstalledPackages() as $package) { foreach (InstalledVersions::getInstalledPackages() as $package) {
@ -43,7 +45,7 @@ final readonly class ComposerNativePackage
|| !is_array($manifest['extra']['typephp-native'] ?? null)) { || !is_array($manifest['extra']['typephp-native'] ?? null)) {
continue; continue;
} }
$packages[] = self::load($package, $compilerRoot); $packages[] = self::load($package, $compilerRoot, $platformName);
} }
usort( usort(
@ -57,7 +59,11 @@ final readonly class ComposerNativePackage
return $packages; 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') { if ($package === 'swoole/php-ext-standard') {
throw new RuntimeException( 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); $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) { foreach ($sources as $source) {
$extension = strtolower(pathinfo($source, PATHINFO_EXTENSION)); $extension = strtolower(pathinfo($source, PATHINFO_EXTENSION));
if (!in_array($extension, ['c', 'cc', 'cpp', 'cxx'], true)) { 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( return new self(
$package, $package,
$root, $root,
@ -219,14 +274,27 @@ final readonly class ComposerNativePackage
$abi, $abi,
$cStandard, $cStandard,
$cxxStandard, $cxxStandard,
self::resolveEntries($root, $native['include-dirs'] ?? null, true, $package), $includeDirs,
$sources, $sources,
array_values(array_unique($defines)),
$components, $components,
$extensionName, $extensionName,
$extensionModuleEntry, $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<string> */ /** @return list<string> */
private static function stringList( private static function stringList(
mixed $values, mixed $values,

@ -2,22 +2,19 @@
namespace TypePhp\Build; 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 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. */ /** Composer package manifests whose C/C++ sources are compiled into the program. */
public const COMPOSER_SOURCES = 'composer-sources'; public const COMPOSER_SOURCES = 'composer-sources';
public static function forHost(string $platformName): string 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 public static function composesRuntimeSources(string $platformName): bool
{ {
return self::forHost($platformName) === self::COMPOSER_SOURCES; return true;
} }
} }

@ -8,7 +8,10 @@ use TypePhp\Analysis\CompilationStatistics;
/** Resolves the Composer-provided runtime sources used by a Nano build. */ /** Resolves the Composer-provided runtime sources used by a Nano build. */
final class NanoSourceComposer 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, ?CompilationStatistics $statistics = null,
): array ): array
{ {
$runtime = ComposerNativePackage::load('swoole/php-nano', $this->compilerRoot); $runtime = ComposerNativePackage::load(
$phpx = ComposerNativePackage::load('swoole/phpx', $this->compilerRoot); 'swoole/php-nano',
$this->compilerRoot,
$this->platformName,
);
$phpx = ComposerNativePackage::load(
'swoole/phpx',
$this->compilerRoot,
$this->platformName,
);
if ($runtime->abi !== $phpx->abi) { if ($runtime->abi !== $phpx->abi) {
throw new RuntimeException( throw new RuntimeException(
"Native ABI mismatch: swoole/php-nano={$runtime->abi}, swoole/phpx={$phpx->abi}" "Native ABI mismatch: swoole/php-nano={$runtime->abi}, swoole/phpx={$phpx->abi}"
@ -41,7 +52,7 @@ final class NanoSourceComposer
$runtime->name => $runtime, $runtime->name => $runtime,
$phpx->name => $phpx, $phpx->name => $phpx,
]; ];
foreach (ComposerNativePackage::discover($this->compilerRoot) as $package) { foreach (ComposerNativePackage::discover($this->compilerRoot, $this->platformName) as $package) {
$packagesByName[$package->name] = $package; $packagesByName[$package->name] = $package;
} }
$packages = array_values($packagesByName); $packages = array_values($packagesByName);
@ -69,6 +80,7 @@ final class NanoSourceComposer
$includeDirs = []; $includeDirs = [];
$defines = []; $defines = [];
foreach ($packages as $package) { foreach ($packages as $package) {
array_push($defines, ...$package->defines);
if ($package->abi !== $runtime->abi) { if ($package->abi !== $runtime->abi) {
throw new RuntimeException( throw new RuntimeException(
"Native ABI mismatch: {$package->name}={$package->abi}, " "Native ABI mismatch: {$package->name}={$package->abi}, "

@ -236,7 +236,16 @@ trait NativeBuildConfigurationTrait
protected function getLibraries(): array protected function getLibraries(): array
{ {
if ($this->isNanoMode()) { if ($this->isNanoMode()) {
return []; return $this->isWindows()
? [
'advapi32.lib',
'bcrypt.lib',
'pathcch.lib',
'shell32.lib',
'user32.lib',
'ws2_32.lib',
]
: [];
} }
$sdkDir = $this->getFullStaticSdkDir(); $sdkDir = $this->getFullStaticSdkDir();

@ -60,7 +60,9 @@ trait NativeCommandOptionsTrait
'sanitize' => $this->sanitize, 'sanitize' => $this->sanitize,
'march' => $this->march, 'march' => $this->march,
'target_platform' => $this->targetPlatform, '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, 'build_mode' => $this->buildMode,
'enable_profiler' => $this->enableProfiler, 'enable_profiler' => $this->enableProfiler,
'prof_output' => $this->targetName . '.prof', 'prof_output' => $this->targetName . '.prof',
@ -187,10 +189,12 @@ trait NativeCommandOptionsTrait
$ldflags = trim('-static -B ' . escapeshellarg($this->getFullStaticMuslDir()) . ' ' . $ldflags); $ldflags = trim('-static -B ' . escapeshellarg($this->getFullStaticMuslDir()) . ' ' . $ldflags);
} }
if ($this->isNanoMode()) { if ($this->isNanoMode()) {
$gcSections = ($this->isMacos() || $this->isIosTarget()) if (!$this->isWindows()) {
? '-Wl,-dead_strip' $gcSections = ($this->isMacos() || $this->isIosTarget())
: '-Wl,--gc-sections'; ? '-Wl,-dead_strip'
$ldflags = trim($gcSections . ' ' . $ldflags); : '-Wl,--gc-sections';
$ldflags = trim($gcSections . ' ' . $ldflags);
}
if ($this->isWasiTarget()) { if ($this->isWasiTarget()) {
$ldflags = trim( $ldflags = trim(
'-fwasm-exceptions -lsetjmp -lunwind ' . $ldflags, '-fwasm-exceptions -lsetjmp -lunwind ' . $ldflags,
@ -208,6 +212,7 @@ trait NativeCommandOptionsTrait
'build_mode' => $this->buildMode, 'build_mode' => $this->buildMode,
'sanitize' => $this->sanitize, 'sanitize' => $this->sanitize,
'lto' => $this->enableLto, 'lto' => $this->enableLto,
'section_gc' => $this->isNanoMode(),
'target_platform' => $targetPlatform, 'target_platform' => $targetPlatform,
'response_file' => $this->getBuildDir() . DIRECTORY_SEPARATOR . 'cache' 'response_file' => $this->getBuildDir() . DIRECTORY_SEPARATOR . 'cache'
. DIRECTORY_SEPARATOR . 'link' . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'link' . DIRECTORY_SEPARATOR

@ -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 private function isForbiddenSymbol(string $symbol, string $target): bool
{ {
if ($target === 'wasip2') { if ($target === 'wasip2') {

@ -25,12 +25,6 @@ final class NativeSourceProjectBuilder
/** @return array{output: string, sourceCount: int, compiledCount: int} */ /** @return array{output: string, sourceCount: int, compiledCount: int} */
public function build(NativeSourceProjectConfig $project): array 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}"); $this->writeProgress("Preparing Nano {$project->target} build: {$project->name}");
$compiler = $this->resolveExecutable($project->compiler); $compiler = $this->resolveExecutable($project->compiler);
@ -58,7 +52,10 @@ final class NativeSourceProjectBuilder
$generatedIncludeDir = $generatedDir . DIRECTORY_SEPARATOR . 'include'; $generatedIncludeDir = $generatedDir . DIRECTORY_SEPARATOR . 'include';
} }
$composition = (new NanoSourceComposer($this->compilerRuntime->installationRoot))->compose( $composition = (new NanoSourceComposer(
$this->compilerRuntime->installationRoot,
PHP_OS_FAMILY,
))->compose(
$project->buildDir, $project->buildDir,
$this->projectSymbolName($project), $this->projectSymbolName($project),
$project->phpSources !== [], $project->phpSources !== [],

@ -335,25 +335,6 @@ abstract class CompilerBase implements PropertyAccessContext
'syslog', '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 = [ private const array NANO_UNSUPPORTED_FUNCTION_PREFIXES = [
'pcntl_', 'pcntl_',
'posix_', 'posix_',
@ -964,21 +945,6 @@ abstract class CompilerBase implements PropertyAccessContext
} }
$name = strtolower(ltrim($name, '\\')); $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)) { if (in_array($name, self::NANO_UNSUPPORTED_FUNCTIONS, true)) {
$this->fatalError($expr, "Function `{$name}` is not supported in nano mode"); $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 public function isBuildModeBin(): bool
{ {
return $this->buildMode === self::BUILD_MODE_BIN; return $this->buildMode === self::BUILD_MODE_BIN;

@ -131,7 +131,7 @@ class Constants
public const array COMPILER_OPTIONS = [ public const array COMPILER_OPTIONS = [
'nano' => [ 'nano' => [
'longPrefix' => '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, 'required' => false,
'noValue' => true, 'noValue' => true,
], ],

@ -483,7 +483,6 @@ abstract class Preprocessor extends CompilerBase
if ($this->isNanoPolicyMode()) { if ($this->isNanoPolicyMode()) {
$traverser->addVisitor(new NanoSyntaxValidationVisitor( $traverser->addVisitor(new NanoSyntaxValidationVisitor(
fn (Node $node, string $message) => $this->fatalError($node, $message), fn (Node $node, string $message) => $this->fatalError($node, $message),
$this->isNanoMode(),
)); ));
} }
$traverser->addVisitor(new VoidCastValidationVisitor( $traverser->addVisitor(new VoidCastValidationVisitor(

@ -18,13 +18,12 @@ final class NanoSyntaxValidationVisitor extends NodeVisitorAbstract
/** @param callable(Node, string): never $fatal */ /** @param callable(Node, string): never $fatal */
public function __construct( public function __construct(
private readonly mixed $fatal, private readonly mixed $fatal,
private readonly bool $phpNanoRuntime = true,
) { ) {
} }
public function enterNode(Node $node): ?Node public function enterNode(Node $node): ?Node
{ {
if ($this->phpNanoRuntime && $node instanceof Node\Name) { if ($node instanceof Node\Name) {
$resolved = $node->getAttribute('resolvedName'); $resolved = $node->getAttribute('resolvedName');
$className = $resolved instanceof Node\Name $className = $resolved instanceof Node\Name
? $resolved->toString() ? $resolved->toString()
@ -60,8 +59,7 @@ final class NanoSyntaxValidationVisitor extends NodeVisitorAbstract
($this->fatal)($node, 'Anonymous classes are not supported in nano mode'); ($this->fatal)($node, 'Anonymous classes are not supported in nano mode');
} }
if ($this->phpNanoRuntime if ($node instanceof Node\Expr\Yield_ || $node instanceof Node\Expr\YieldFrom) {
&& ($node instanceof Node\Expr\Yield_ || $node instanceof Node\Expr\YieldFrom)) {
($this->fatal)( ($this->fatal)(
$node, $node,
'Fiber and Generator are not supported in nano mode because C++17 has no standard stack-switching API', 'Fiber and Generator are not supported in nano mode because C++17 has no standard stack-switching API',

@ -449,7 +449,7 @@ class Translator extends Preprocessor
['--march <arch>', 'Target CPU instruction set (for example native or armv8-a)'], ['--march <arch>', 'Target CPU instruction set (for example native or armv8-a)'],
['--target-platform <triple>', 'Cross-compilation target triple'], ['--target-platform <triple>', 'Cross-compilation target triple'],
['--wasm[=browser|component]', 'Build WASI component (default) or browser output'], ['--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'], ['--full-static', 'Link fully statically against the bundled SDK'],
['--lto', 'Enable Link Time Optimization (-flto)'], ['--lto', 'Enable Link Time Optimization (-flto)'],
['--no-literal-strings', 'Disable literal string optimization'], ['--no-literal-strings', 'Disable literal string optimization'],
@ -493,14 +493,16 @@ class Translator extends Preprocessor
$this->downloadProxy = $proxy; $this->downloadProxy = $proxy;
} }
// The Nano syntax policy is platform-independent. Windows produces a // Every native platform composes the same php-nano and PHPX runtime
// native application through the PHP/PHPX DLL backend; other targets // sources directly into the output artifact.
// compose the php-nano runtime sources into the artifact.
if ($this->climate->arguments->defined('nano')) { if ($this->climate->arguments->defined('nano')) {
$this->nanoPolicyMode = true; $this->nanoPolicyMode = true;
if (NanoBuildBackend::composesRuntimeSources($this->getPlatform()->getName())) { if (NanoBuildBackend::composesRuntimeSources($this->getPlatform()->getName())) {
$this->nanoMode = true; $this->nanoMode = true;
$this->noLiteralStrings = 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'); $this->error('--nano requires the C++17 language standard');
} }
if ($this->fullStatic) { if ($this->fullStatic) {
$message = $this->isNanoMode() $this->error('--nano already composes its runtime sources; --full-static is not applicable');
? '--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);
} }
// Nano applications may consume target-owned static libraries. // Nano applications may consume target-owned static libraries.
// The final executable dependency audit remains the authority on // The final executable dependency audit remains the authority on
@ -1838,30 +1837,6 @@ CODE;
// module_clean end // module_clean end
$moduleName = $this->getModuleName(); $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_function *>(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 // rinit begin
$code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL; $code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL;
$code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL; $code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL;
@ -1878,18 +1853,6 @@ CODE;
$code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL; $code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL;
$code .= '}' . PHP_EOL; $code .= '}' . PHP_EOL;
$code .= 'php::request_init();' . 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; $code .= 'module_init();' . PHP_EOL;
if ($this->isBuildModeBin() && $this->hasSapi('embed') && !$this->isNanoMode()) { if ($this->isBuildModeBin() && $this->hasSapi('embed') && !$this->isNanoMode()) {
@ -1897,42 +1860,28 @@ CODE;
$code .= 'if (strcmp(sapi_module.name, "embed") == 0) {' . PHP_EOL; $code .= 'if (strcmp(sapi_module.name, "embed") == 0) {' . PHP_EOL;
} }
$entryFunction = $this->symbols->function(self::ENTRY_FUNCTION); $entryFunction = $this->symbols->function(self::ENTRY_FUNCTION);
if ($this->isNanoPolicyMode()) { // FunctionDef::sourceFile comes from loadFile()'s realpath(), so the
// Windows keeps the complete PHP/PHPX DLL runtime, but a Nano // CLI script fields always identify main()'s canonical absolute file.
// executable still enters generated code without ZendVM eval. $entryFile = $entryFunction->sourceFile;
$code .= $this->registerServerEnvironment($entryFunction->sourceFile); $entryFileArg = $this->genCharPtr($entryFile, true);
$entryCall = count($entryFunction->argInfoList) === 2 $entryLineOffset = max(0, $entryFunction->startLine - 1);
? 'php_main(php::global("argc").toInt(), php::global("argv").toArray());' if (count($entryFunction->argInfoList) == 2) {
: 'php_main();'; $entryScript = 'global $argc, $argv; main($argc, $argv);';
$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;
} else { } else {
// FunctionDef::sourceFile comes from loadFile()'s realpath(), so the $entryScript = 'main();';
// 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;
}
$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()) { if ($this->isSapiBuild()) {
$code .= '}' . PHP_EOL; $code .= '}' . PHP_EOL;
} }
@ -2604,7 +2553,10 @@ CODE;
/** @param list<string> $generatedSources @return list<string> */ /** @param list<string> $generatedSources @return list<string> */
private function composeNanoRuntimeSources(array $generatedSources): array 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->getBuildDir(),
$this->targetName, $this->targetName,
true, true,
@ -3060,6 +3012,12 @@ CODE;
); );
return; return;
} }
if ($this->isWindows()) {
$auditor->assertWindowsImports(
$this->captureNativeCommand(['dumpbin', '/imports', $targetFile]),
);
return;
}
/* Native objects may deliberately provide an OS ABI to one another /* Native objects may deliberately provide an OS ABI to one another
* (for example a freestanding syscall shim). Only unresolved imports * (for example a freestanding syscall shim). Only unresolved imports
* in the final ELF cross the host-capability boundary. */ * in the final ELF cross the host-capability boundary. */
@ -3583,25 +3541,6 @@ CODE;
$declarationHeaders[] = $header; $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([ return $this->renderIncludeHeaderFiles([
@ -4844,7 +4783,6 @@ CODE;
if ($this->isNanoPolicyMode()) { if ($this->isNanoPolicyMode()) {
$traverser->addVisitor(new NanoSyntaxValidationVisitor( $traverser->addVisitor(new NanoSyntaxValidationVisitor(
fn (Node $node, string $message) => $this->fatalError($node, $message), fn (Node $node, string $message) => $this->fatalError($node, $message),
$this->isNanoMode(),
)); ));
} }
$traverser->addVisitor(new VoidCastValidationVisitor( $traverser->addVisitor(new VoidCastValidationVisitor(

Loading…
Cancel
Save