diff --git a/ext/sqlite3/JitSqlite3.php b/ext/sqlite3/JitSqlite3.php index 3c6a326a7ca..f9b84f304aa 100644 --- a/ext/sqlite3/JitSqlite3.php +++ b/ext/sqlite3/JitSqlite3.php @@ -32,7 +32,8 @@ * zim_SQLite3_querySingle / zim_SQLite3_lastInsertRowID / zim_SQLite3_changes / * zim_SQLite3_lastErrorCode / zim_SQLite3_lastErrorMsg (#35966) / * zim_SQLite3_busyTimeout (#35972) / zim_SQLite3_enableExceptions (#35975) / - * zim_SQLite3_escapeString (#35977) / zim_SQLite3_version (#35991 leftover of #35977) + * zim_SQLite3_escapeString (#35977) / zim_SQLite3_version (#35991 leftover of #35977) / + * zim_SQLite3_open (#36001 leftover of #35991) */ final class JitSqlite3 { @@ -172,6 +173,84 @@ public static function close(Context $context, JITVariable ...$args): Value return self::boxBool($context, true); } + /** + * SQLite3::open leftover of version (#36001 / #35991). + * php-src zim_sqlite3_open: throw Exception when already open; reopen after close. + * Thin AOT folds open/reopen onto {@see Sqlite3JitSupport} props (same honesty class as + * __construct — no libsqlite3 FFI at runtime). + */ + public static function open(Context $context, JITVariable ...$args): Value + { + if (!VmClassMethod::requireJitUserArgCountRange($context, $args, 'SQLite3::open', 1, 3)) { + return VmClassMethod::jitArgcDummyReturn($context); + } + $obj = self::readObject($context, $args[0]); + JitStringBuiltinArg::lower( + $context, + $args[1], + 'SQLite3::open', + 1, + 'filename' + ); + if (\count($args) >= 3) { + JitLongArg::lower($context, $args[2], 'SQLite3::open(): Argument #2 ($flags)'); + } + if (\count($args) >= 4) { + JitStringBuiltinArg::lower( + $context, + $args[3], + 'SQLite3::open', + 3, + 'encryption_key' + ); + } + + $i64 = $context->getTypeFromString('int64'); + $id = self::loadLong($context, $obj, Sqlite3JitSupport::PROP_ID); + $alreadyOpen = $context->builder->icmp( + \PHPLLVM\Builder::INT_NE, + $id, + $i64->constInt(0, false) + ); + $bbThrow = BasicBlockHelper::append($context, 'sqlite3_open_already'); + $bbOk = BasicBlockHelper::append($context, 'sqlite3_open_ok'); + $context->builder->branchIf($alreadyOpen, $bbThrow, $bbOk); + + $context->builder->positionAtEnd($bbThrow); + if (null !== \PHPCompiler\JIT\TryCatchHelper::resolveThrowHandler($context)) { + \PHPCompiler\JIT\TryCatchHelper::emitCatchableClassError( + $context, + 'Exception', + 'Already initialised DB Object' + ); + } else { + ExceptionBridge::emitErrorAndAbort($context, 'Already initialised DB Object'); + } + $seal = BasicBlockHelper::tryGetInsertBlock($context); + if (null !== $seal && null === $seal->getTerminator()) { + ExceptionBridge::emitErrorAndAbort($context, 'Already initialised DB Object'); + } + + $context->builder->positionAtEnd($bbOk); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_ID, $i64->constInt(1, false)); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_ROW, $i64->constInt(0, false)); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_HAS, $i64->constInt(0, false)); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_LAST_ROWID, $i64->constInt(0, false)); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_CHANGES, $i64->constInt(0, false)); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_ROW_COUNT, $i64->constInt(0, false)); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_SUM, $i64->constInt(0, false)); + self::storeLong($context, $obj, Sqlite3JitSupport::PROP_INT_PK, $i64->constInt(0, false)); + // Keep PROP_EXCEPTIONS across close/reopen (php-src retains exception mode on the object). + + $slot = JitValueBox::alloc($context); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + JitValueBox::pointer($context, $slot) + ); + + return JitValueBox::pointer($context, $slot); + } + public static function lastInsertRowID(Context $context, JITVariable ...$args): Value { if (!VmClassMethod::requireExactJitUserArgCount($context, $args, 'SQLite3::lastInsertRowID', 0)) { diff --git a/lib/JIT/Call/Sqlite3Method.php b/lib/JIT/Call/Sqlite3Method.php index 99bc03e97c1..525b0c50360 100644 --- a/lib/JIT/Call/Sqlite3Method.php +++ b/lib/JIT/Call/Sqlite3Method.php @@ -13,10 +13,10 @@ /** * SQLite3 thin-AOT methods — __construct / exec / querySingle / close / * lastInsertRowID / changes / lastErrorCode / lastErrorMsg / busyTimeout / - * enableExceptions / escapeString / version + * enableExceptions / escapeString / version / open * (#35931 leftover of #35914; lastError leftover #35966; busyTimeout leftover #35972; * enableExceptions leftover #35975; escapeString leftover #35977; - * version leftover #35991). + * version leftover #35991; open leftover #36001). * * php-src: ext/sqlite3/sqlite3.c */ @@ -55,6 +55,8 @@ public function __construct( // Static ZEND_METHOD — no implicit $this for SQLite3::version() (#35991). $this->paramNames = []; $this->namedArgsReceiverPrefix = 0; + } elseif ('open' === $lc) { + $this->paramNames = ['filename', 'flags=', 'encryption_key=']; } } @@ -78,8 +80,9 @@ public function call(Context $context, Variable ...$args): Value 'enableexceptions' => JitSqlite3::enableExceptions($context, ...$args), 'escapestring' => JitSqlite3::escapeString($context, ...$args), 'version' => JitSqlite3::version($context, ...$args), + 'open' => JitSqlite3::open($context, ...$args), default => throw new \LogicException( - 'SQLite3::'.$this->method.'() JIT dispatch missing (#35931 / #35991)' + 'SQLite3::'.$this->method.'() JIT dispatch missing (#35931 / #35991 / #36001)' ), }; } diff --git a/lib/JIT/Context.php b/lib/JIT/Context.php index c7ab95e6c3a..e3e75bd6214 100644 --- a/lib/JIT/Context.php +++ b/lib/JIT/Context.php @@ -2296,10 +2296,10 @@ private function defineBuiltins(int $loadType): void { $zipMethod ); } - // SQLite3 construct/exec/querySingle/lastInsertRowID/changes/lastError*/busyTimeout/enableExceptions/escapeString/version — NestedJIT leftover of advertise-only AOT (#35931 / #35966 / #35972 / #35975 / #35977 / #35991 / #20565). + // SQLite3 construct/exec/querySingle/lastInsertRowID/changes/lastError*/busyTimeout/enableExceptions/escapeString/version/open — NestedJIT leftover of advertise-only AOT (#35931 / #35966 / #35972 / #35975 / #35977 / #35991 / #36001 / #20565). if (CompilerVersion::supportsSqlite3()) { $this->type->object->lookup('SQLite3'); - foreach (['__construct', 'exec', 'querySingle', 'close', 'lastInsertRowID', 'changes', 'lastErrorCode', 'lastErrorMsg', 'busyTimeout', 'enableExceptions', 'escapeString', 'version'] as $sqliteMethod) { + foreach (['__construct', 'exec', 'querySingle', 'close', 'lastInsertRowID', 'changes', 'lastErrorCode', 'lastErrorMsg', 'busyTimeout', 'enableExceptions', 'escapeString', 'version', 'open'] as $sqliteMethod) { $this->functionProxies['sqlite3::'.strtolower($sqliteMethod)] = new Call\Sqlite3Method( $sqliteMethod ); diff --git a/test/repro/sqlite3_open_aot.php b/test/repro/sqlite3_open_aot.php new file mode 100644 index 00000000000..0b52a049f83 --- /dev/null +++ b/test/repro/sqlite3_open_aot.php @@ -0,0 +1,21 @@ +open(':memory:'); + echo "no-throw\n"; +} catch (Throwable $e) { + echo get_class($e), ':', $e->getMessage(), "\n"; +} +echo 'close=', var_export($db->close(), true), "\n"; +try { + $r = $db->open(':memory:'); + echo 'reopen=', var_export($r, true), "\n"; +} catch (Throwable $e) { + echo 'reopen-ex:', get_class($e), ':', $e->getMessage(), "\n"; +} +$db->exec('CREATE TABLE t(x); INSERT INTO t VALUES (7);'); +echo 'q=', var_export($db->querySingle('SELECT x FROM t'), true), "\n"; diff --git a/test/unit/Sqlite3OpenAotTest.php b/test/unit/Sqlite3OpenAotTest.php new file mode 100644 index 00000000000..15a65ffe5bf --- /dev/null +++ b/test/unit/Sqlite3OpenAotTest.php @@ -0,0 +1,94 @@ +markTestSkipped('LLVM 9 toolchain not available'); + } + + $src = __DIR__.'/../repro/sqlite3_open_aot.php'; + $this->assertSame($this->runVm($src), $this->runAot($src)); + } + + public function testProxyRegisteredForOpen(): void + { + $root = dirname(__DIR__, 2); + $src = (string) file_get_contents($root.'/lib/JIT/Context.php'); + $this->assertStringContainsString("'version', 'open'", $src); + $dispatch = (string) file_get_contents($root.'/lib/JIT/Call/Sqlite3Method.php'); + $this->assertStringContainsString("'open'", $dispatch); + $jit = (string) file_get_contents($root.'/ext/sqlite3/JitSqlite3.php'); + $this->assertStringContainsString('function open(', $jit); + $this->assertStringContainsString('#36001', $jit); + $this->assertFileDoesNotExist($root.'/lib/AOT/runtime/sqlite3_open.c'); + $this->assertFileDoesNotExist($root.'/runtime/sqlite3_open.c'); + } + + private function runVm(string $src): string + { + return $this->runEnv(['PHP_COMPILER_PROFILE=8.4'], 'bin/vm.php', $src); + } + + private function runAot(string $src): string + { + $root = dirname(__DIR__, 2); + $bin = sys_get_temp_dir().'/sq3_open_'.getmypid().'_'.md5($src); + $compile = 'env PHP_COMPILER_PROFILE=8.4 PHP_COMPILER_HELPER_RUNTIME_O=0 ' + .escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php').' -o ' + .escapeshellarg($bin).' ' + .escapeshellarg($src); + $cwd = getcwd(); + chdir($root); + try { + exec($compile.' 2>&1', $cout, $crc); + $this->assertSame(0, $crc, implode("\n", $cout)); + $this->assertFileExists($bin); + exec(escapeshellarg($bin).' 2>&1', $out, $rc); + $this->assertSame(0, $rc, implode("\n", $out)); + + return implode("\n", $out); + } finally { + @unlink($bin); + chdir($cwd); + } + } + + /** + * @param list $env + */ + private function runEnv(array $env, string $binRel, string $src): string + { + $root = dirname(__DIR__, 2); + $cmd = 'env '.implode(' ', $env).' ' + .escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/'.$binRel).' ' + .escapeshellarg($src); + $cwd = getcwd(); + chdir($root); + try { + exec($cmd.' 2>&1', $out, $rc); + $this->assertSame(0, $rc, implode("\n", $out)); + + return implode("\n", $out); + } finally { + chdir($cwd); + } + } +}