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. 44
      .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. 28
      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. 7
      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. 96
      src/Translator.php

@ -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') {
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/')

@ -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 代码生成、并行任务调度、编译进度条、输出路径规则以及

@ -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,

@ -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": {

@ -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 模式
*/

@ -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 = [

@ -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'],

@ -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",
);
}
}

@ -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);

@ -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, "<?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

@ -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(
'<?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.
$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,9 +116,14 @@ class Msvc extends CompilerBackend
if (!empty($config['forced_include'])) {
$cmd .= ' /FI' . escapeshellarg($config['forced_include']);
}
} elseif (!empty($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;
}

@ -11,6 +11,7 @@ final readonly class ComposerNativePackage
/**
* @param list<string> $includeDirs
* @param list<string> $sources
* @param list<string> $defines
* @param array<string, ComposerNativeComponent> $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<self> */
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<string> */
private static function stringList(
mixed $values,

@ -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;
}
}

@ -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}, "

@ -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();

@ -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()) {
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

@ -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') {

@ -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 !== [],

@ -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;

@ -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,
],

@ -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(

@ -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',

@ -449,7 +449,7 @@ class Translator extends Preprocessor
['--march <arch>', 'Target CPU instruction set (for example native or armv8-a)'],
['--target-platform <triple>', '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_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
$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,19 +1860,6 @@ 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;
} else {
// FunctionDef::sourceFile comes from loadFile()'s realpath(), so the
// CLI script fields always identify main()'s canonical absolute file.
$entryFile = $entryFunction->sourceFile;
@ -1932,7 +1882,6 @@ CODE;
}
$code .= 'php::eval(' . $entryScriptArg . ', ' . $entryFileArg . ');' . PHP_EOL;
}
if ($this->isSapiBuild()) {
$code .= '}' . PHP_EOL;
}
@ -2604,7 +2553,10 @@ CODE;
/** @param list<string> $generatedSources @return list<string> */
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(

Loading…
Cancel
Save