aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--examples/php-on-wasm/php-wasm.php15
-rw-r--r--examples/rubyvm-on-php-on-wasm/php-wasm.php32
-rw-r--r--src/BinaryFormat/Decoder.php145
-rw-r--r--src/Stream/FileStream.php123
-rw-r--r--src/Stream/IoException.php11
-rw-r--r--src/Stream/StreamInterface.php77
-rw-r--r--src/Stream/UnexpectedEofException.php11
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
+{
+}