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
韩天峰 3 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
*.class
*.wasm
/swoole_compiler
/tpc
tests/**/*.diff
tests/**/*.exp
@ -30,3 +29,4 @@ tests/**/*.log
tests/**/*.out
tests/**/*.php
tests/**/*.sh
/browser/node_modules/

@ -29,16 +29,18 @@ final class WasiToolchainTest extends TestCase
public function testDetectsSupportedToolsOnlyFromPath(): void
{
$this->installFakeTools(22, 47, 'wasm32-unknown-wasip1');
$this->installFakeTools(22, 47, 1, 'wasm32-unknown-wasip2');
putenv('PATH=' . $this->directory);
$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('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('47.0.0', $tools['wasmtime-version']);
$this->assertSame('1.0.0', $tools['jco-version']);
}
public function testRejectsMissingTool(): void
@ -46,40 +48,42 @@ final class WasiToolchainTest extends TestCase
putenv('PATH=' . $this->directory);
$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();
}
public function testRejectsOldLlvm(): void
{
$this->installFakeTools(21, 47, 'wasm32-unknown-wasip1');
$this->installFakeTools(21, 47, 1, 'wasm32-unknown-wasip2');
putenv('PATH=' . $this->directory);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('`clang` 21 is too old');
$this->expectExceptionMessage('`wasm32-wasip2-clang` 21 is too old');
(new WasiToolchain())->detect();
}
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);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('not configured for wasm32-wasi');
$this->expectExceptionMessage('not configured for wasm32-unknown-wasip2');
(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('wasm-component-ld', "#!/bin/sh\necho 'wasm-component-ld version 0.5.22'\n");
$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",
);
$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

@ -101,12 +101,12 @@ class CompilerBaseApiTest extends TestCase
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->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->assertFalse($this->compiler->isWasiTarget(), $target);
}

@ -12,7 +12,7 @@ class FiberGeneratorTest extends TestCase
{
$compiler = CompilerTest::create(ROOT_PATH);
$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';
$compiler->addFiles([$file]);

@ -12,9 +12,9 @@ class PlatformTest extends TestCase
{
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('.wasm', $platform->getExecutableExtension());
$this->assertSame('.a', $platform->getSharedLibraryExtension());

@ -12,7 +12,7 @@ class WasiUnsupportedSyntaxTest extends TestCase
public function testUnsupportedSyntaxFailsDuringWasiConversion(string $file, string $message): void
{
$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;
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
@ -37,6 +37,18 @@ class WasiUnsupportedSyntaxTest extends TestCase
'wasi-generator-arrow.php',
'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_WASMTIME_MAJOR = 47;
public const MIN_JCO_MAJOR = 1;
/** @return array<string, string> */
public function detect(): array
{
$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);
}
$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);
}
$this->requireVersion('wasm-component-ld', $tools['wasm-component-ld'], 0);
$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);
if ($exitCode !== 0 || preg_match('/^wasm32-(?:unknown-)?wasi(?:p1)?$/', $target) !== 1) {
if ($exitCode !== 0 || $target !== 'wasm32-unknown-wasip2') {
$detail = trim($error) !== '' ? ': ' . trim($error) : '';
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}",
);
}
$tools['clang'] = $tools['wasm32-wasip2-clang'];
$tools['clang++'] = $tools['wasm32-wasip2-clang++'];
$tools['wasm-ld'] = $tools['wasm-component-ld'];
$tools['target'] = $target;
$tools['clang-version'] = $versions['clang++'];
$tools['clang-version'] = $versions['wasm32-wasip2-clang++'];
$tools['wasmtime-version'] = $versions['wasmtime'];
$tools['jco-version'] = $versions['jco'];
return $tools;
}
@ -62,7 +78,7 @@ final class WasiToolchain
{
[$exitCode, $output, $error] = $this->run([$executable, '--version']);
$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");
}
$major = (int) $match[2];

@ -174,6 +174,41 @@ class CompilerBase implements PropertyAccessContext
'stream_socket_accept',
'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_PROPERTY = 2;
public const int DECL_TYPE_OF_CONST = 3;
@ -584,7 +619,24 @@ class CompilerBase implements PropertyAccessContext
public function isWasiTarget(): bool
{
$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

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

@ -4,7 +4,7 @@ namespace TypePhp\Platform;
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')
? (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');
$this->platform = new Wasi(
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_LD'] = $tools['wasm-ld'];
$environment['TYPEPHP_WASMTIME'] = $tools['wasmtime'];
$environment['TYPEPHP_JCO'] = $tools['jco'];
$environment['TYPEPHP_WASI_TARGET'] = $tools['target'];
$environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version'];
$environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version'];
$environment['TYPEPHP_JCO_VERSION'] = $tools['jco-version'];
$environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $buildDir;
$process = proc_open(

Loading…
Cancel
Save