diff options
| -rw-r--r-- | examples/php-on-wasm/php-wasm.php | 15 | ||||
| -rw-r--r-- | examples/rubyvm-on-php-on-wasm/php-wasm.php | 32 | ||||
| -rw-r--r-- | src/BinaryFormat/Decoder.php | 145 | ||||
| -rw-r--r-- | src/Stream/FileStream.php | 123 | ||||
| -rw-r--r-- | src/Stream/IoException.php | 11 | ||||
| -rw-r--r-- | src/Stream/StreamInterface.php | 77 | ||||
| -rw-r--r-- | src/Stream/UnexpectedEofException.php | 11 |
7 files changed, 282 insertions, 132 deletions
diff --git a/examples/php-on-wasm/php-wasm.php b/examples/php-on-wasm/php-wasm.php index 92871cd..697b5b3 100644 --- a/examples/php-on-wasm/php-wasm.php +++ b/examples/php-on-wasm/php-wasm.php @@ -5,33 +5,24 @@ declare(strict_types=1); require_once __DIR__ . '/../../vendor/autoload.php'; use Nsfisis\Waddiwasi\BinaryFormat\Decoder; -use Nsfisis\Waddiwasi\BinaryFormat\InvalidBinaryFormatException; use Nsfisis\Waddiwasi\Execution\Extern; use Nsfisis\Waddiwasi\Execution\Externs; use Nsfisis\Waddiwasi\Execution\FuncInst; use Nsfisis\Waddiwasi\Execution\Refs; use Nsfisis\Waddiwasi\Execution\Runtime; use Nsfisis\Waddiwasi\Execution\Store; +use Nsfisis\Waddiwasi\Stream\FileStream; use Nsfisis\Waddiwasi\Structure\Types\FuncType; use Nsfisis\Waddiwasi\Structure\Types\NumType; use Nsfisis\Waddiwasi\Structure\Types\ResultType; use Nsfisis\Waddiwasi\Structure\Types\ValType; -const PHP_EMPTY = ''; - const PHP_HELLO_WORLD = <<<'EOS' echo "Hello, World!\n"; EOS; -$wasmBinary = file_get_contents(__DIR__ . '/php-wasm.wasm'); -\assert($wasmBinary !== false); - -try { - $module = (new Decoder($wasmBinary))->decode(); -} catch (InvalidBinaryFormatException $e) { - fprintf(STDERR, $e->getMessage() . "\n"); - exit(1); -} +$wasmBinaryStream = new FileStream(__DIR__ . '/php-wasm.wasm'); +$module = (new Decoder($wasmBinaryStream))->decode(); $imports = [ 'env' => [ diff --git a/examples/rubyvm-on-php-on-wasm/php-wasm.php b/examples/rubyvm-on-php-on-wasm/php-wasm.php index b24641d..723e210 100644 --- a/examples/rubyvm-on-php-on-wasm/php-wasm.php +++ b/examples/rubyvm-on-php-on-wasm/php-wasm.php @@ -5,34 +5,24 @@ declare(strict_types=1); require_once __DIR__ . '/vendor/autoload.php'; use Nsfisis\Waddiwasi\BinaryFormat\Decoder; -use Nsfisis\Waddiwasi\BinaryFormat\InvalidBinaryFormatException; use Nsfisis\Waddiwasi\Execution\Extern; use Nsfisis\Waddiwasi\Execution\Externs; use Nsfisis\Waddiwasi\Execution\FuncInst; -use Nsfisis\Waddiwasi\Execution\MemInst; use Nsfisis\Waddiwasi\Execution\Refs; use Nsfisis\Waddiwasi\Execution\Runtime; use Nsfisis\Waddiwasi\Execution\Store; +use Nsfisis\Waddiwasi\Stream\FileStream; use Nsfisis\Waddiwasi\Structure\Types\FuncType; use Nsfisis\Waddiwasi\Structure\Types\NumType; use Nsfisis\Waddiwasi\Structure\Types\ResultType; use Nsfisis\Waddiwasi\Structure\Types\ValType; -const PHP_EMPTY = ''; - const PHP_HELLO_WORLD = <<<'EOS' require_once '%DIR%/HelloWorld.php'; EOS; -$wasmBinary = file_get_contents(__DIR__ . '/php-wasm.wasm'); -\assert($wasmBinary !== false); - -try { - $module = (new Decoder($wasmBinary))->decode(); -} catch (InvalidBinaryFormatException $e) { - fprintf(STDERR, $e->getMessage() . "\n"); - exit(1); -} +$wasmBinaryStream = new FileStream(__DIR__ . '/php-wasm.wasm'); +$module = (new Decoder($wasmBinaryStream))->decode(); $imports = [ 'env' => [ @@ -142,22 +132,6 @@ $results = $runtime->invoke("php_wasm_run", [$codePtr]); $exitCode = $results[0]; \assert(\is_int($exitCode)); -function dumpMemory(MemInst $mem): void -{ - $buf = ''; - $s = $mem->size(); - for ($j = 0; $j < $s; $j++) { - $c = $mem->loadByte($j); - \assert($c !== null); - $buf .= \chr($c); - if ($j % 1024 === 1023) { - fputs(STDOUT, $buf); - $buf = ""; - } - } - fputs(STDOUT, $buf); -} - function allocateStringOnWasmMemory(Runtime $runtime, string $str): int { // Plus 1 for the null terminator in C. diff --git a/src/BinaryFormat/Decoder.php b/src/BinaryFormat/Decoder.php index 3c23fc8..14079e1 100644 --- a/src/BinaryFormat/Decoder.php +++ b/src/BinaryFormat/Decoder.php @@ -7,6 +7,7 @@ namespace Nsfisis\Waddiwasi\BinaryFormat; use Nsfisis\Waddiwasi\BinaryFormat\Internal\Code; use Nsfisis\Waddiwasi\BinaryFormat\Internal\Locals; use Nsfisis\Waddiwasi\BinaryFormat\Internal\SectionId; +use Nsfisis\Waddiwasi\Stream\StreamInterface; use Nsfisis\Waddiwasi\Structure\Instructions\Instr; use Nsfisis\Waddiwasi\Structure\Instructions\Instrs; use Nsfisis\Waddiwasi\Structure\Instructions\Instrs\Control\BlockType; @@ -44,19 +45,12 @@ use function in_array; use function is_float; use function is_int; use function ord; -use function strlen; final class Decoder { - private string $input; - private int $inputSize; - private int $pos; - - public function __construct(string $wasmBinary) - { - $this->input = $wasmBinary; - $this->inputSize = strlen($wasmBinary); - $this->pos = 0; + public function __construct( + private readonly StreamInterface $stream, + ) { } public function decode(): Module @@ -77,7 +71,7 @@ final class Decoder $codes = $this->decodeSection(SectionId::Code, $this->decodeCodeSecRest(...)) ?? []; $datas = $this->decodeSection(SectionId::Data, $this->decodeDataSecRest(...)) ?? []; - if (!$this->eof()) { + if (!$this->stream->eof()) { throw new InvalidBinaryFormatException("eof"); } if ($dataCount === null) { @@ -125,30 +119,26 @@ final class Decoder private function checkMagic(): void { - assert($this->pos === 0); - $this->ensureNBytesRemains(4); - $b1 = ord($this->input[0]); - $b2 = ord($this->input[1]); - $b3 = ord($this->input[2]); - $b4 = ord($this->input[3]); + $bs = $this->stream->read(4); + $b1 = ord($bs[0]); + $b2 = ord($bs[1]); + $b3 = ord($bs[2]); + $b4 = ord($bs[3]); if ([$b1, $b2, $b3, $b4] !== [0x00, 0x61, 0x73, 0x6D]) { throw new InvalidBinaryFormatException("magic"); } - $this->pos += 4; } private function checkVersion(): void { - assert($this->pos === 4); - $this->ensureNBytesRemains(4); - $b1 = ord($this->input[4]); - $b2 = ord($this->input[5]); - $b3 = ord($this->input[6]); - $b4 = ord($this->input[7]); + $bs = $this->stream->read(4); + $b1 = ord($bs[0]); + $b2 = ord($bs[1]); + $b3 = ord($bs[2]); + $b4 = ord($bs[3]); if ([$b1, $b2, $b3, $b4] !== [0x01, 0x00, 0x00, 0x00]) { throw new InvalidBinaryFormatException(sprintf("version: [%x, %x, %x, %x]", $b1, $b2, $b3, $b4)); } - $this->pos += 4; } /** @@ -159,11 +149,11 @@ final class Decoder private function decodeSection(SectionId $sectionId, callable $decoder): mixed { $this->skipCustomSections(); - if ($this->eof()) { + if ($this->stream->eof()) { return null; } - $idValue = $this->peekByte(); + $idValue = $this->stream->peekByte(); $id = SectionId::tryFrom($idValue); if ($id === null) { throw new InvalidBinaryFormatException("section id"); @@ -171,12 +161,12 @@ final class Decoder if ($id !== $sectionId) { return null; } - $this->skipNBytes(1); + $this->stream->seek(1); $size = $this->decodeU32(); - $prevPos = $this->pos; + $prevPos = $this->stream->tell(); $result = $decoder(); - if ($this->pos - $prevPos !== $size) { + if ($this->stream->tell() - $prevPos !== $size) { throw new InvalidBinaryFormatException("type section size"); } return $result; @@ -184,17 +174,23 @@ final class Decoder private function skipCustomSections(): void { - while (!$this->eof()) { - $b = $this->peekByte(); + while (!$this->stream->eof()) { + $b = $this->stream->peekByte(); if ($b !== SectionId::Custom->value) { break; } - $this->skipNBytes(1); + $this->stream->seek(1); $size = $this->decodeU32(); - $prevPos = $this->pos; + $prevPos = $this->stream->tell(); $this->decodeName(); - $encodedSizeOfName = $this->pos - $prevPos; - $this->skipNBytes($size - $encodedSizeOfName); + $encodedSizeOfName = $this->stream->tell() - $prevPos; + $offset = $size - $encodedSizeOfName; + if ($offset < 0) { + throw new InvalidBinaryFormatException("custom section size"); + } + if ($offset !== 0) { + $this->stream->seek($offset); + } } } @@ -312,21 +308,21 @@ final class Decoder private function decodeValType(): ValType { - $b = $this->peekByte(); + $b = $this->stream->peekByte(); if ($b === 0x7F) { - $this->skipNBytes(1); + $this->stream->seek(1); return ValType::NumType(NumType::I32); } elseif ($b === 0x7E) { - $this->skipNBytes(1); + $this->stream->seek(1); return ValType::NumType(NumType::I64); } elseif ($b === 0x7D) { - $this->skipNBytes(1); + $this->stream->seek(1); return ValType::NumType(NumType::F32); } elseif ($b === 0x7C) { - $this->skipNBytes(1); + $this->stream->seek(1); return ValType::NumType(NumType::F64); } elseif ($b === 0x7B) { - $this->skipNBytes(1); + $this->stream->seek(1); return ValType::VecType(VecType::V128); } else { return ValType::RefType($this->decodeRefType()); @@ -524,10 +520,10 @@ final class Decoder private function decodeCode(): Code { $size = $this->decodeU32(); - $prevPos = $this->pos; + $prevPos = $this->stream->tell(); $compressedLocals = $this->decodeVec($this->decodeLocals(...)); $body = $this->decodeExpr(); - if ($this->pos - $prevPos !== $size) { + if ($this->stream->tell() - $prevPos !== $size) { throw new InvalidBinaryFormatException("code size"); } return new Code( @@ -672,7 +668,7 @@ final class Decoder private function decodeInstr(): Instr { - switch ($this->decodeByte()) { + switch ($op = $this->decodeByte()) { case 0x00: return Instr::Unreachable(); case 0x01: return Instr::Nop(); case 0x02: @@ -739,14 +735,15 @@ final class Decoder case 0x3D: return Instr::I64Store16(...$this->decodeMemArg()); case 0x3E: return Instr::I64Store32(...$this->decodeMemArg()); case 0x3F: - if ($this->decodeByte() !== 0) { - $this->seekBy(-1); - throw new InvalidBinaryFormatException("Unexpected value while decoding an instruction `memory.size`, expected 0, but got " . $this->decodeByte()); + $c = $this->decodeByte(); + if ($c !== 0) { + throw new InvalidBinaryFormatException("Unexpected value while decoding an instruction `memory.size`, expected 0, but got $c"); } return Instr::MemorySize(); case 0x40: - if ($this->decodeByte() !== 0) { - throw new InvalidBinaryFormatException("memory grow"); + $c = $this->decodeByte(); + if ($c !== 0) { + throw new InvalidBinaryFormatException("Unexpected value while decoding an instruction `memory.grow`, expected 0, but got $c"); } return Instr::MemoryGrow(); case 0x41: return Instr::I32Const($this->decodeS32()); @@ -931,8 +928,6 @@ final class Decoder } // no break default: - $this->seekBy(-1); - $op = $this->decodeByte(); throw new InvalidBinaryFormatException("Unexpected opcode $op while decoding an instruction"); } } @@ -949,9 +944,9 @@ final class Decoder private function decodeBlockType(): BlockType { - $b = $this->peekByte(); + $b = $this->stream->peekByte(); if ($b === 0x40) { - $this->skipNBytes(1); + $this->stream->seek(1); return BlockType::ValType(null); } elseif (in_array($b, [0x7F, 0x7E, 0x7D, 0x7C, 0x7B, 0x70, 0x6F], true)) { return BlockType::ValType($this->decodeValType()); @@ -995,42 +990,12 @@ final class Decoder return $result; } - private function eof(): bool - { - return strlen($this->input) <= $this->pos; - } - - private function ensureNBytesRemains(int $n): void - { - if ($this->inputSize < $this->pos + $n) { - throw new InvalidBinaryFormatException("ensureNBytesRemains: $this->inputSize < $this->pos + $n"); - } - } - - private function skipNBytes(int $n): void - { - $this->ensureNBytesRemains($n); - $this->pos += $n; - } - - private function peekByte(): int - { - $this->ensureNBytesRemains(1); - return ord($this->input[$this->pos]); - } - /** * @phpstan-impure */ private function decodeByte(): int { - $this->ensureNBytesRemains(1); - return ord($this->input[$this->pos++]); - } - - private function seekBy(int $offset): void - { - $this->pos += $offset; + return $this->stream->readByte(); } private function decodeU32(): int @@ -1115,9 +1080,8 @@ final class Decoder */ private function decodeF32(): float { - $this->ensureNBytesRemains(4); - $result = unpack('g', $this->input, $this->pos); - $this->pos += 4; + $buf = $this->stream->read(4); + $result = unpack('g', $buf); if ($result === false) { throw new InvalidBinaryFormatException("f32"); } @@ -1130,9 +1094,8 @@ final class Decoder */ private function decodeF64(): float { - $this->ensureNBytesRemains(8); - $result = unpack('e', $this->input, $this->pos); - $this->pos += 8; + $buf = $this->stream->read(8); + $result = unpack('e', $buf); if ($result === false) { throw new InvalidBinaryFormatException("f64"); } diff --git a/src/Stream/FileStream.php b/src/Stream/FileStream.php new file mode 100644 index 0000000..cb5b519 --- /dev/null +++ b/src/Stream/FileStream.php @@ -0,0 +1,123 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +use function assert; +use function chr; +use function fclose; +use function fopen; +use function fread; +use function ord; +use function strlen; + +final class FileStream implements StreamInterface +{ + /** + * @var resource + */ + private readonly mixed $fp; + + /** + * @var ?int<0, 255> + */ + private ?int $peekedByte = null; + + public function __construct( + private readonly string $path, + ) { + $fp = fopen($path, 'rb'); + if ($fp === false) { + throw new IoException("Failed to open file: $path"); + } + $this->fp = $fp; + } + + public function close(): void + { + fclose($this->fp); + } + + public function read(int $bytes): string + { + if ($this->peekedByte !== null) { + $first = chr($this->peekedByte); + $this->peekedByte = null; + if ($bytes === 1) { + return $first; + } else { + return $first . $this->doRead($bytes - 1); + } + } else { + return $this->doRead($bytes); + } + } + + public function readByte(): int + { + if ($this->peekedByte !== null) { + $ret = $this->peekedByte; + $this->peekedByte = null; + return $ret; + } + return ord($this->doRead(1)); + } + + public function peekByte(): int + { + if ($this->peekedByte === null) { + $this->peekedByte = ord($this->doRead(1)); + } + return $this->peekedByte; + } + + public function seek(int $bytes): void + { + $this->read($bytes); + } + + public function tell(): int + { + $ret = ftell($this->fp); + if ($ret === false) { + throw new IoException("Failed to get current position in file: $this->path"); + } + assert(0 <= $ret); + return $ret; + } + + public function eof(): bool + { + // feof() does not work because it returns true only after an + // unsuccessful fread(). + if ($this->peekedByte !== null) { + return false; + } + $result = fread($this->fp, 1); + if ($result === false || $result === '') { + return true; + } + $this->peekedByte = ord($result); + return false; + } + + /** + * @param positive-int $bytes + * + * @return non-empty-string + * + * @phpstan-impure + */ + private function doRead(int $bytes): string + { + $result = fread($this->fp, $bytes); + if ($result === false) { + throw new IoException("Failed to read from file: $this->path"); + } + if (strlen($result) < $bytes) { + throw new UnexpectedEofException(sprintf("Unexpected EOF while reading from file: %s (%d bytes expected, %d bytes read)", $this->path, $bytes, strlen($result))); + } + return $result; + } +} diff --git a/src/Stream/IoException.php b/src/Stream/IoException.php new file mode 100644 index 0000000..bdd0723 --- /dev/null +++ b/src/Stream/IoException.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +use RuntimeException; + +final class IoException extends RuntimeException +{ +} diff --git a/src/Stream/StreamInterface.php b/src/Stream/StreamInterface.php new file mode 100644 index 0000000..0655d22 --- /dev/null +++ b/src/Stream/StreamInterface.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +interface StreamInterface +{ + /** + * Reads $bytes bytes from the stream. + * + * @param positive-int $bytes + * + * @return non-empty-string + * A binary string of $bytes bytes. + * + * @throws UnexpectedEofException + * Thrown if the stream does not have enough bytes to read. + * + * @phpstan-impure + */ + public function read(int $bytes): string; + + /** + * Reads a single byte from the stream. + * + * @return int<0, 255> + * An 8-bit unsigned integer read from the stream. + * + * @throws UnexpectedEofException + * Thrown if the stream have reached the end. + * + * @phpstan-impure + */ + public function readByte(): int; + + /** + * Reads a single byte from the stream without advancing the position. + * + * @return int<0, 255> + * An 8-bit unsigned integer read from the stream. + * + * @throws UnexpectedEofException + * Thrown if the stream have reached the end. + * + * @phpstan-impure + */ + public function peekByte(): int; + + /** + * Seeks $bytes bytes from the current position. + * + * @param positive-int $bytes + * + * @throws UnexpectedEofException + * Thrown if the stream does not have enough bytes to seek. + * + * @phpstan-impure + */ + public function seek(int $bytes): void; + + /** + * Returns the current position in the stream. + * + * @return 0|positive-int + * + * @phpstan-impure + */ + public function tell(): int; + + /** + * Returns whether the stream has reached the end. + * + * @phpstan-impure + */ + public function eof(): bool; +} diff --git a/src/Stream/UnexpectedEofException.php b/src/Stream/UnexpectedEofException.php new file mode 100644 index 0000000..c4780ab --- /dev/null +++ b/src/Stream/UnexpectedEofException.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +use RuntimeException; + +final class UnexpectedEofException extends RuntimeException +{ +} |
