feat(compiler): upgrade WASI support to Preview 2 with enhanced function validation

- Update WASI target detection to support wasm32-wasip2 and wasm32-unknown-wasip2 only
- Add jco tool integration with version requirement and environment variables
- Implement comprehensive function validation for WASI unsupported functions
- Add WASI_UNSUPPORTED_FUNCTIONS list including exec, socket, pcntl, and posix functions
- Replace wasm-ld with wasm-component-ld for WASI Preview 2 compatibility
- Remove support for deprecated WASI Preview 1 targets with proper error messaging
- Update toolchain detection to use wasip2-specific compiler tools
- Add function prefix validation for pcntl_, posix_, and socket_ function families
- Enhance version detection regex pattern for more reliable tool version parsing
- Update test suite to validate new WASI Preview 2 target and tooling requirements
pull/46/head
韩天峰 4 weeks ago
parent 8503c7c32c
commit 9992793a02
  1. 2
      .gitignore
  2. 26
      phpunit/src/Build/WasiToolchainTest.php
  3. 4
      phpunit/src/CompilerBaseApiTest.php
  4. 2
      phpunit/src/Generator/FiberGeneratorTest.php
  5. 4
      phpunit/src/Platform/PlatformTest.php
  6. 14
      phpunit/src/WasiUnsupportedSyntaxTest.php
  7. 30
      src/Build/WasiToolchain.php
  8. 54
      src/CompilerBase.php
  9. 3
      src/Parser/FunctionCallTrait.php
  10. 2
      src/Platform/Wasi.php
  11. 5
      src/Translator.php
  12. 2
      src/compiler.php

2
.gitignore vendored

@ -22,7 +22,6 @@
*.exp *.exp
*.class *.class
*.wasm *.wasm
/swoole_compiler
/tpc /tpc
tests/**/*.diff tests/**/*.diff
tests/**/*.exp tests/**/*.exp
@ -30,3 +29,4 @@ tests/**/*.log
tests/**/*.out tests/**/*.out
tests/**/*.php tests/**/*.php
tests/**/*.sh tests/**/*.sh
/browser/node_modules/

@ -29,16 +29,18 @@ final class WasiToolchainTest extends TestCase
public function testDetectsSupportedToolsOnlyFromPath(): void public function testDetectsSupportedToolsOnlyFromPath(): void
{ {
$this->installFakeTools(22, 47, 'wasm32-unknown-wasip1'); $this->installFakeTools(22, 47, 1, 'wasm32-unknown-wasip2');
putenv('PATH=' . $this->directory); putenv('PATH=' . $this->directory);
$tools = (new WasiToolchain())->detect(); $tools = (new WasiToolchain())->detect();
$this->assertSame($this->directory . '/clang++', $tools['clang++']); $this->assertSame($this->directory . '/wasm32-wasip2-clang++', $tools['clang++']);
$this->assertSame($this->directory . '/wasmtime', $tools['wasmtime']); $this->assertSame($this->directory . '/wasmtime', $tools['wasmtime']);
$this->assertSame('wasm32-unknown-wasip1', $tools['target']); $this->assertSame($this->directory . '/jco', $tools['jco']);
$this->assertSame('wasm32-unknown-wasip2', $tools['target']);
$this->assertSame('22.0.0', $tools['clang-version']); $this->assertSame('22.0.0', $tools['clang-version']);
$this->assertSame('47.0.0', $tools['wasmtime-version']); $this->assertSame('47.0.0', $tools['wasmtime-version']);
$this->assertSame('1.0.0', $tools['jco-version']);
} }
public function testRejectsMissingTool(): void public function testRejectsMissingTool(): void
@ -46,40 +48,42 @@ final class WasiToolchainTest extends TestCase
putenv('PATH=' . $this->directory); putenv('PATH=' . $this->directory);
$this->expectException(RuntimeException::class); $this->expectException(RuntimeException::class);
$this->expectExceptionMessage('`clang` was not found in PATH'); $this->expectExceptionMessage('`wasm32-wasip2-clang` was not found in PATH');
(new WasiToolchain())->detect(); (new WasiToolchain())->detect();
} }
public function testRejectsOldLlvm(): void public function testRejectsOldLlvm(): void
{ {
$this->installFakeTools(21, 47, 'wasm32-unknown-wasip1'); $this->installFakeTools(21, 47, 1, 'wasm32-unknown-wasip2');
putenv('PATH=' . $this->directory); putenv('PATH=' . $this->directory);
$this->expectException(RuntimeException::class); $this->expectException(RuntimeException::class);
$this->expectExceptionMessage('`clang` 21 is too old'); $this->expectExceptionMessage('`wasm32-wasip2-clang` 21 is too old');
(new WasiToolchain())->detect(); (new WasiToolchain())->detect();
} }
public function testRejectsNonWasiClangTarget(): void public function testRejectsNonWasiClangTarget(): void
{ {
$this->installFakeTools(22, 47, 'x86_64-unknown-linux-gnu'); $this->installFakeTools(22, 47, 1, 'x86_64-unknown-linux-gnu');
putenv('PATH=' . $this->directory); putenv('PATH=' . $this->directory);
$this->expectException(RuntimeException::class); $this->expectException(RuntimeException::class);
$this->expectExceptionMessage('not configured for wasm32-wasi'); $this->expectExceptionMessage('not configured for wasm32-unknown-wasip2');
(new WasiToolchain())->detect(); (new WasiToolchain())->detect();
} }
private function installFakeTools(int $llvmMajor, int $wasmtimeMajor, string $target): void private function installFakeTools(int $llvmMajor, int $wasmtimeMajor, int $jcoMajor, string $target): void
{ {
foreach (['clang', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld'] as $tool) { foreach (['wasm32-wasip2-clang', 'llvm-ar', 'llvm-ranlib', 'llvm-nm'] as $tool) {
$this->writeExecutable($tool, "#!/bin/sh\necho 'LLVM version {$llvmMajor}.0.0'\n"); $this->writeExecutable($tool, "#!/bin/sh\necho 'LLVM version {$llvmMajor}.0.0'\n");
} }
$this->writeExecutable('wasm-component-ld', "#!/bin/sh\necho 'wasm-component-ld version 0.5.22'\n");
$this->writeExecutable( $this->writeExecutable(
'clang++', 'wasm32-wasip2-clang++',
"#!/bin/sh\nif [ \"\$1\" = '--print-target-triple' ]; then echo '{$target}'; else echo 'clang version {$llvmMajor}.0.0'; fi\n", "#!/bin/sh\nif [ \"\$1\" = '--print-target-triple' ]; then echo '{$target}'; else echo 'clang version {$llvmMajor}.0.0'; fi\n",
); );
$this->writeExecutable('wasmtime', "#!/bin/sh\necho 'wasmtime {$wasmtimeMajor}.0.0'\n"); $this->writeExecutable('wasmtime', "#!/bin/sh\necho 'wasmtime {$wasmtimeMajor}.0.0'\n");
$this->writeExecutable('jco', "#!/bin/sh\necho 'jco {$jcoMajor}.0.0'\n");
} }
private function writeExecutable(string $name, string $contents): void private function writeExecutable(string $name, string $contents): void

@ -101,12 +101,12 @@ class CompilerBaseApiTest extends TestCase
public function testWasiTargetDetection(): void public function testWasiTargetDetection(): void
{ {
foreach (['wasm32-wasi', 'wasm32-wasip1', 'wasm32-wasip1-threads'] as $target) { foreach (['wasm32-wasip2', 'wasm32-unknown-wasip2'] as $target) {
$this->setPropertyValue('targetPlatform', $target); $this->setPropertyValue('targetPlatform', $target);
$this->assertTrue($this->compiler->isWasiTarget(), $target); $this->assertTrue($this->compiler->isWasiTarget(), $target);
} }
foreach (['', 'wasm32-unknown-unknown', 'aarch64-linux-gnu'] as $target) { foreach (['', 'wasm32-wasi', 'wasm32-wasip1', 'wasm32-wasip1-threads', 'wasm32-unknown-unknown', 'aarch64-linux-gnu'] as $target) {
$this->setPropertyValue('targetPlatform', $target); $this->setPropertyValue('targetPlatform', $target);
$this->assertFalse($this->compiler->isWasiTarget(), $target); $this->assertFalse($this->compiler->isWasiTarget(), $target);
} }

@ -12,7 +12,7 @@ class FiberGeneratorTest extends TestCase
{ {
$compiler = CompilerTest::create(ROOT_PATH); $compiler = CompilerTest::create(ROOT_PATH);
$reflection = new \ReflectionClass($compiler); $reflection = new \ReflectionClass($compiler);
$reflection->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasi'); $reflection->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasip2');
$file = __DIR__ . '/../../code/generator-conversion-error.php'; $file = __DIR__ . '/../../code/generator-conversion-error.php';
$compiler->addFiles([$file]); $compiler->addFiles([$file]);

@ -12,9 +12,9 @@ class PlatformTest extends TestCase
{ {
public function testWasiTargetProperties(): void public function testWasiTargetProperties(): void
{ {
$platform = new Wasi('wasm32-unknown-wasip1'); $platform = new Wasi('wasm32-unknown-wasip2');
$this->assertSame('WASI SDK (wasm32-unknown-wasip1)', $platform->getName()); $this->assertSame('WASI SDK (wasm32-unknown-wasip2)', $platform->getName());
$this->assertSame('.o', $platform->getObjectExtension()); $this->assertSame('.o', $platform->getObjectExtension());
$this->assertSame('.wasm', $platform->getExecutableExtension()); $this->assertSame('.wasm', $platform->getExecutableExtension());
$this->assertSame('.a', $platform->getSharedLibraryExtension()); $this->assertSame('.a', $platform->getSharedLibraryExtension());

@ -12,7 +12,7 @@ class WasiUnsupportedSyntaxTest extends TestCase
public function testUnsupportedSyntaxFailsDuringWasiConversion(string $file, string $message): void public function testUnsupportedSyntaxFailsDuringWasiConversion(string $file, string $message): void
{ {
$compiler = CompilerTest::create(ROOT_PATH); $compiler = CompilerTest::create(ROOT_PATH);
(new \ReflectionClass($compiler))->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasi'); (new \ReflectionClass($compiler))->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasip2');
$source = __DIR__ . '/../code/' . $file; $source = __DIR__ . '/../code/' . $file;
$compiler->addFiles([$source]); $compiler->addFiles([$source]);
$compiler->prepareFile($source); $compiler->prepareFile($source);
@ -37,6 +37,18 @@ class WasiUnsupportedSyntaxTest extends TestCase
'wasi-generator-arrow.php', 'wasi-generator-arrow.php',
'Fiber and Generator are not supported by the WASI target', 'Fiber and Generator are not supported by the WASI target',
], ],
'process function' => [
'wasi-process.php',
'Function `proc_open` is not supported by the WASI target',
],
'socket function' => [
'wasi-socket.php',
'Function `stream_socket_server` is not supported by the WASI target',
],
'signal function' => [
'wasi-signal.php',
'Function `pcntl_signal` is not supported by the WASI target',
],
]; ];
} }
} }

@ -8,34 +8,50 @@ final class WasiToolchain
{ {
public const MIN_LLVM_MAJOR = 22; public const MIN_LLVM_MAJOR = 22;
public const MIN_WASMTIME_MAJOR = 47; public const MIN_WASMTIME_MAJOR = 47;
public const MIN_JCO_MAJOR = 1;
/** @return array<string, string> */ /** @return array<string, string> */
public function detect(): array public function detect(): array
{ {
$tools = []; $tools = [];
foreach (['clang', 'clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld', 'wasmtime'] as $name) { foreach ([
'wasm32-wasip2-clang',
'wasm32-wasip2-clang++',
'llvm-ar',
'llvm-ranlib',
'llvm-nm',
'wasm-component-ld',
'wasmtime',
'jco',
] as $name) {
$tools[$name] = $this->findExecutable($name); $tools[$name] = $this->findExecutable($name);
} }
$versions = []; $versions = [];
foreach (['clang', 'clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld'] as $name) { foreach (['wasm32-wasip2-clang', 'wasm32-wasip2-clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm'] as $name) {
$versions[$name] = $this->requireVersion($name, $tools[$name], self::MIN_LLVM_MAJOR); $versions[$name] = $this->requireVersion($name, $tools[$name], self::MIN_LLVM_MAJOR);
} }
$this->requireVersion('wasm-component-ld', $tools['wasm-component-ld'], 0);
$versions['wasmtime'] = $this->requireVersion('wasmtime', $tools['wasmtime'], self::MIN_WASMTIME_MAJOR); $versions['wasmtime'] = $this->requireVersion('wasmtime', $tools['wasmtime'], self::MIN_WASMTIME_MAJOR);
$versions['jco'] = $this->requireVersion('jco', $tools['jco'], self::MIN_JCO_MAJOR);
[$exitCode, $target, $error] = $this->run([$tools['clang++'], '--print-target-triple']); [$exitCode, $target, $error] = $this->run([$tools['wasm32-wasip2-clang++'], '--print-target-triple']);
$target = trim($target); $target = trim($target);
if ($exitCode !== 0 || preg_match('/^wasm32-(?:unknown-)?wasi(?:p1)?$/', $target) !== 1) { if ($exitCode !== 0 || $target !== 'wasm32-unknown-wasip2') {
$detail = trim($error) !== '' ? ': ' . trim($error) : ''; $detail = trim($error) !== '' ? ': ' . trim($error) : '';
throw new RuntimeException( throw new RuntimeException(
"clang++ from PATH is not configured for wasm32-wasi (reported target: " "wasm32-wasip2-clang++ from PATH is not configured for wasm32-unknown-wasip2 (reported target: "
. ($target !== '' ? $target : 'unknown') . "){$detail}", . ($target !== '' ? $target : 'unknown') . "){$detail}",
); );
} }
$tools['clang'] = $tools['wasm32-wasip2-clang'];
$tools['clang++'] = $tools['wasm32-wasip2-clang++'];
$tools['wasm-ld'] = $tools['wasm-component-ld'];
$tools['target'] = $target; $tools['target'] = $target;
$tools['clang-version'] = $versions['clang++']; $tools['clang-version'] = $versions['wasm32-wasip2-clang++'];
$tools['wasmtime-version'] = $versions['wasmtime']; $tools['wasmtime-version'] = $versions['wasmtime'];
$tools['jco-version'] = $versions['jco'];
return $tools; return $tools;
} }
@ -62,7 +78,7 @@ final class WasiToolchain
{ {
[$exitCode, $output, $error] = $this->run([$executable, '--version']); [$exitCode, $output, $error] = $this->run([$executable, '--version']);
$versionText = trim($output . "\n" . $error); $versionText = trim($output . "\n" . $error);
if ($exitCode !== 0 || preg_match('/(?:version|wasmtime|LLD)\s+((\d+)(?:\.\d+)+)/i', $versionText, $match) !== 1) { if ($exitCode !== 0 || preg_match('/\bv?((\d+)(?:\.\d+)+)\b/i', $versionText, $match) !== 1) {
throw new RuntimeException("Unable to determine the version of WASI tool `{$name}` from PATH"); throw new RuntimeException("Unable to determine the version of WASI tool `{$name}` from PATH");
} }
$major = (int) $match[2]; $major = (int) $match[2];

@ -174,6 +174,41 @@ class CompilerBase implements PropertyAccessContext
'stream_socket_accept', 'stream_socket_accept',
'popen', 'popen',
]; ];
/**
* APIs which cannot have the same semantics in Wasmtime and a browser.
* Keep this list at the language boundary so a WASI build never degrades
* into a link error or a browser-only implementation.
*/
private const array WASI_UNSUPPORTED_FUNCTIONS = [
'exec',
'passthru',
'popen',
'proc_close',
'proc_get_status',
'proc_nice',
'proc_open',
'proc_terminate',
'shell_exec',
'system',
'fsockopen',
'pfsockopen',
'stream_socket_accept',
'stream_socket_client',
'stream_socket_enable_crypto',
'stream_socket_get_name',
'stream_socket_pair',
'stream_socket_recvfrom',
'stream_socket_sendto',
'stream_socket_server',
'stream_socket_shutdown',
];
private const array WASI_UNSUPPORTED_FUNCTION_PREFIXES = [
'pcntl_',
'posix_',
'socket_',
];
public const int DECL_TYPE_OF_RETURN = 1; public const int DECL_TYPE_OF_RETURN = 1;
public const int DECL_TYPE_OF_PROPERTY = 2; public const int DECL_TYPE_OF_PROPERTY = 2;
public const int DECL_TYPE_OF_CONST = 3; public const int DECL_TYPE_OF_CONST = 3;
@ -584,7 +619,24 @@ class CompilerBase implements PropertyAccessContext
public function isWasiTarget(): bool public function isWasiTarget(): bool
{ {
$target = strtolower($this->targetPlatform); $target = strtolower($this->targetPlatform);
return str_starts_with($target, 'wasm32-wasi') || str_starts_with($target, 'wasm32-wasip1'); return $target === 'wasm32-unknown-wasip2' || $target === 'wasm32-wasip2';
}
protected function assertWasiFunctionSupported(NodeAbstract $expr, string $name): void
{
if (!$this->isWasiTarget()) {
return;
}
$name = strtolower(ltrim($name, '\\'));
if (in_array($name, self::WASI_UNSUPPORTED_FUNCTIONS, true)) {
$this->fatalError($expr, "Function `{$name}` is not supported by the WASI target");
}
foreach (self::WASI_UNSUPPORTED_FUNCTION_PREFIXES as $prefix) {
if (str_starts_with($name, $prefix)) {
$this->fatalError($expr, "Function `{$name}` is not supported by the WASI target");
}
}
} }
public function isBuildModeBin(): bool public function isBuildModeBin(): bool

@ -78,6 +78,9 @@ trait FunctionCallTrait
} elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') { } elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') {
$name = $this->parseIdentifier($expr->name); $name = $this->parseIdentifier($expr->name);
$globalName = ltrim($name, '\\'); $globalName = ltrim($name, '\\');
if ($this->isInternalFunction($globalName)) {
$this->assertWasiFunctionSupported($expr, $globalName);
}
if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) { if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) {
$this->fatalError($expr, 'Unsupported function: `' . $name . '`'); $this->fatalError($expr, 'Unsupported function: `' . $name . '`');
} }

@ -4,7 +4,7 @@ namespace TypePhp\Platform;
final class Wasi extends UnixPlatform final class Wasi extends UnixPlatform
{ {
public function __construct(private readonly string $target = 'wasm32-wasi') public function __construct(private readonly string $target = 'wasm32-unknown-wasip2')
{ {
} }

@ -168,7 +168,10 @@ class Translator extends Preprocessor
$targetPlatform = $this->climate->arguments->defined('target-platform') $targetPlatform = $this->climate->arguments->defined('target-platform')
? (string) $this->climate->arguments->get('target-platform') ? (string) $this->climate->arguments->get('target-platform')
: ''; : '';
if (str_starts_with($targetPlatform, 'wasm32-wasi') || str_starts_with($targetPlatform, 'wasm32-wasip1')) { if ($targetPlatform === 'wasm32-wasip1' || $targetPlatform === 'wasm32-wasi') {
throw new \RuntimeException('WASI Preview 1 is not supported; use wasm32-wasip2');
}
if ($targetPlatform === 'wasm32-wasip2' || $targetPlatform === 'wasm32-unknown-wasip2') {
$detectedTarget = getenv('TYPEPHP_WASI_TARGET'); $detectedTarget = getenv('TYPEPHP_WASI_TARGET');
$this->platform = new Wasi( $this->platform = new Wasi(
is_string($detectedTarget) && $detectedTarget !== '' ? $detectedTarget : $targetPlatform, is_string($detectedTarget) && $detectedTarget !== '' ? $detectedTarget : $targetPlatform,

@ -140,9 +140,11 @@ function compileWasmProgram(array $argv): void
$environment['TYPEPHP_WASI_NM'] = $tools['llvm-nm']; $environment['TYPEPHP_WASI_NM'] = $tools['llvm-nm'];
$environment['TYPEPHP_WASI_LD'] = $tools['wasm-ld']; $environment['TYPEPHP_WASI_LD'] = $tools['wasm-ld'];
$environment['TYPEPHP_WASMTIME'] = $tools['wasmtime']; $environment['TYPEPHP_WASMTIME'] = $tools['wasmtime'];
$environment['TYPEPHP_JCO'] = $tools['jco'];
$environment['TYPEPHP_WASI_TARGET'] = $tools['target']; $environment['TYPEPHP_WASI_TARGET'] = $tools['target'];
$environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version']; $environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version'];
$environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version']; $environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version'];
$environment['TYPEPHP_JCO_VERSION'] = $tools['jco-version'];
$environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $buildDir; $environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $buildDir;
$process = proc_open( $process = proc_open(

Loading…
Cancel
Save