diff --git a/ext/sysvsem/Module.php b/ext/sysvsem/Module.php new file mode 100644 index 00000000000..409331e3281 --- /dev/null +++ b/ext/sysvsem/Module.php @@ -0,0 +1,36 @@ +vmContext); + } + + public static function registerClasses(Context $ctx): void + { + VmSem::registerClass($ctx); + } + + public function getFunctions(): array + { + return [ + new sem_get(), + new sem_acquire(), + new sem_release(), + new sem_remove(), + ]; + } +} diff --git a/ext/sysvsem/SemArgs.php b/ext/sysvsem/SemArgs.php new file mode 100644 index 00000000000..f42f79411d5 --- /dev/null +++ b/ext/sysvsem/SemArgs.php @@ -0,0 +1,125 @@ +calledArgs[$index]->resolveIndirect(); + if (Variable::TYPE_INTEGER !== $var->type) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($%s) must be of type int, %s given', + $fn, + $index + 1, + 'key', + VmStreamArg::debugTypeName($var) + )); + } + + return $var->toInt(); + } + + public static function parseOptionalInt(Frame $frame, int $index, string $fn, string $param): ?int + { + if (!isset($frame->calledArgs[$index])) { + return null; + } + $var = $frame->calledArgs[$index]->resolveIndirect(); + if (Variable::TYPE_NULL === $var->type) { + return null; + } + if (Variable::TYPE_INTEGER !== $var->type) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($%s) must be of type int, %s given', + $fn, + $index + 1, + $param, + VmStreamArg::debugTypeName($var) + )); + } + + return $var->toInt(); + } + + public static function parseOptionalBool(Frame $frame, int $index, string $fn, string $param): ?bool + { + if (!isset($frame->calledArgs[$index])) { + return null; + } + $var = $frame->calledArgs[$index]->resolveIndirect(); + if (Variable::TYPE_NULL === $var->type) { + return null; + } + if (Variable::TYPE_BOOL !== $var->type) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($%s) must be of type bool, %s given', + $fn, + $index + 1, + $param, + VmStreamArg::debugTypeName($var) + )); + } + + return $var->toBool(); + } + + public static function parseSemaphore(Frame $frame, string $fn, int $index = 0): ObjectEntry + { + $var = $frame->calledArgs[$index]; + $resolved = $var->resolveIndirect(); + if (EnumCaseSupport::isEnumCaseVariable($resolved)) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($semaphore) must be of type SysvSemaphore, %s given', + $fn, + $index + 1, + EnumCaseSupport::typeNameForVariable($resolved) + )); + } + if (Variable::TYPE_OBJECT !== $resolved->type) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($semaphore) must be of type SysvSemaphore, %s given', + $fn, + $index + 1, + VmStreamArg::debugTypeName($resolved) + )); + } + $object = $resolved->toObject(); + if (!VmSem::isSysvSemaphoreObject($object)) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($semaphore) must be of type SysvSemaphore, %s given', + $fn, + $index + 1, + $object->class->name + )); + } + + return $object; + } + + public static function requireHost(ObjectEntry $object, string $fn): object + { + $host = VmSem::hostForObject($object); + if (null === $host) { + throw new \ValueError($fn.'(): Argument #1 ($semaphore) has already been detached'); + } + + return $host; + } +} diff --git a/ext/sysvsem/VmSem.php b/ext/sysvsem/VmSem.php new file mode 100644 index 00000000000..768896f95d9 --- /dev/null +++ b/ext/sysvsem/VmSem.php @@ -0,0 +1,126 @@ + VM object id => host SysvSemaphore */ + private static array $hostByObjectId = []; + + public static function available(): bool + { + return \function_exists('sem_get'); + } + + public static function registerClass(Context $ctx): void + { + if (isset($ctx->classes[self::CLASS_LC])) { + return; + } + + $entry = new ClassEntry('SysvSemaphore'); + $entry->isInternal = true; + $ctx->classes[self::CLASS_LC] = $entry; + } + + public static function wrapHost(Context $ctx, object $host): Variable + { + self::registerClass($ctx); + $var = new Variable(Variable::TYPE_OBJECT); + $object = new ObjectEntry($ctx->classes[self::CLASS_LC]); + $object->constructed = true; + self::$hostByObjectId[$object->id] = $host; + $var->object($object); + + return $var; + } + + public static function hostForObject(ObjectEntry $object): ?object + { + if (0 !== strcasecmp($object->class->name, 'SysvSemaphore')) { + return null; + } + + return self::$hostByObjectId[$object->id] ?? null; + } + + public static function detachObject(ObjectEntry $object): void + { + unset(self::$hostByObjectId[$object->id]); + } + + public static function isSysvSemaphoreObject(?ObjectEntry $object): bool + { + return null !== $object && 0 === strcasecmp($object->class->name, 'SysvSemaphore'); + } + + /** + * @return array{0: Variable|false, 1: string} + */ + public static function get(Context $ctx, int $key, ?int $maxAcquire, ?int $perm, ?int $autoRelease): array + { + if (!self::available()) { + return [false, 'sem_get() is not available in this compiler build']; + } + + if (null === $maxAcquire && null === $perm && null === $autoRelease) { + $host = @\sem_get($key); + } elseif (null === $perm && null === $autoRelease) { + $host = @\sem_get($key, $maxAcquire); + } elseif (null === $autoRelease) { + $host = @\sem_get($key, $maxAcquire, $perm); + } else { + $host = @\sem_get($key, $maxAcquire, $perm, $autoRelease); + } + + if (false === $host || !\is_object($host)) { + $last = \error_get_last(); + $message = \is_array($last) && isset($last['message']) ? (string) $last['message'] : 'sem_get() failed'; + + return [false, $message]; + } + + return [self::wrapHost($ctx, $host), '']; + } + + public static function acquire(object $host, bool $nowait = false): bool + { + if (!self::available()) { + return false; + } + + return @\sem_acquire($host, $nowait); + } + + public static function release(object $host): bool + { + if (!self::available()) { + return false; + } + + return @\sem_release($host); + } + + public static function remove(object $host): bool + { + if (!self::available()) { + return false; + } + + return @\sem_remove($host); + } +} diff --git a/ext/sysvsem/sem_acquire.php b/ext/sysvsem/sem_acquire.php new file mode 100644 index 00000000000..67fae504186 --- /dev/null +++ b/ext/sysvsem/sem_acquire.php @@ -0,0 +1,66 @@ +calledArgs); + if ($argc < 1 || $argc > 2) { + throw new \ArgumentCountError( + 'sem_acquire() expects at least 1 argument, '.$argc.' given' + ); + } + if (null === $frame->returnVar) { + return; + } + + SemArgs::requireAvailable('sem_acquire'); + $object = SemArgs::parseSemaphore($frame, 'sem_acquire'); + $host = SemArgs::requireHost($object, 'sem_acquire'); + $nowait = SemArgs::parseOptionalBool($frame, 1, 'sem_acquire', 'nowait') ?? false; + + $result = VmSem::acquire($host, $nowait); + if (!$result) { + $this->triggerWarning($frame, 'sem_acquire() failed'); + } + $frame->returnVar->bool($result); + } + + public function call(Context $context, JITVariable ...$args): Value + { + throw new \LogicException( + 'sem_acquire() is not supported for JIT/AOT in this compiler build (issue #3704)' + ); + } + + private function triggerWarning(Frame $frame, string $message): void + { + if (null === $frame->vmContext) { + return; + } + $frame->vmContext->errors->triggerError( + $message, + ErrorReporter::E_WARNING, + '' !== $frame->scriptPath ? $frame->scriptPath : null, + $frame->vmContext, + $frame + ); + } +} diff --git a/ext/sysvsem/sem_get.php b/ext/sysvsem/sem_get.php new file mode 100644 index 00000000000..5ba4c06c2ad --- /dev/null +++ b/ext/sysvsem/sem_get.php @@ -0,0 +1,77 @@ +calledArgs); + if ($argc < 1 || $argc > 4) { + throw new \ArgumentCountError( + 'sem_get() expects at least 1 argument, '.$argc.' given' + ); + } + if (null === $frame->returnVar) { + return; + } + + SemArgs::requireAvailable('sem_get'); + $key = SemArgs::parseKey($frame, 'sem_get'); + $maxAcquire = SemArgs::parseOptionalInt($frame, 1, 'sem_get', 'max_acquire'); + $perm = SemArgs::parseOptionalInt($frame, 2, 'sem_get', 'perm'); + $autoRelease = SemArgs::parseOptionalInt($frame, 3, 'sem_get', 'auto_release'); + + [$result, $message] = VmSem::get( + $frame->vmContext, + $key, + $maxAcquire, + $perm, + $autoRelease + ); + if (false === $result) { + $this->triggerWarning($frame, $message); + $frame->returnVar->bool(false); + + return; + } + + $frame->returnVar->copyFrom($result); + } + + public function call(Context $context, JITVariable ...$args): Value + { + throw new \LogicException( + 'sem_get() is not supported for JIT/AOT in this compiler build (issue #3704)' + ); + } + + private function triggerWarning(Frame $frame, string $message): void + { + if (null === $frame->vmContext) { + return; + } + $frame->vmContext->errors->triggerError( + $message, + ErrorReporter::E_WARNING, + '' !== $frame->scriptPath ? $frame->scriptPath : null, + $frame->vmContext, + $frame + ); + } +} diff --git a/ext/sysvsem/sem_release.php b/ext/sysvsem/sem_release.php new file mode 100644 index 00000000000..96526a890c6 --- /dev/null +++ b/ext/sysvsem/sem_release.php @@ -0,0 +1,64 @@ +calledArgs)) { + throw new \ArgumentCountError( + 'sem_release() expects exactly 1 argument, '.\count($frame->calledArgs).' given' + ); + } + if (null === $frame->returnVar) { + return; + } + + SemArgs::requireAvailable('sem_release'); + $object = SemArgs::parseSemaphore($frame, 'sem_release'); + $host = SemArgs::requireHost($object, 'sem_release'); + + $result = VmSem::release($host); + if (!$result) { + $this->triggerWarning($frame, 'sem_release() failed'); + } + $frame->returnVar->bool($result); + } + + public function call(Context $context, JITVariable ...$args): Value + { + throw new \LogicException( + 'sem_release() is not supported for JIT/AOT in this compiler build (issue #3704)' + ); + } + + private function triggerWarning(Frame $frame, string $message): void + { + if (null === $frame->vmContext) { + return; + } + $frame->vmContext->errors->triggerError( + $message, + ErrorReporter::E_WARNING, + '' !== $frame->scriptPath ? $frame->scriptPath : null, + $frame->vmContext, + $frame + ); + } +} diff --git a/ext/sysvsem/sem_remove.php b/ext/sysvsem/sem_remove.php new file mode 100644 index 00000000000..ab5337e5816 --- /dev/null +++ b/ext/sysvsem/sem_remove.php @@ -0,0 +1,66 @@ +calledArgs)) { + throw new \ArgumentCountError( + 'sem_remove() expects exactly 1 argument, '.\count($frame->calledArgs).' given' + ); + } + if (null === $frame->returnVar) { + return; + } + + SemArgs::requireAvailable('sem_remove'); + $object = SemArgs::parseSemaphore($frame, 'sem_remove'); + $host = SemArgs::requireHost($object, 'sem_remove'); + + $result = VmSem::remove($host); + if ($result) { + VmSem::detachObject($object); + } else { + $this->triggerWarning($frame, 'sem_remove() failed'); + } + $frame->returnVar->bool($result); + } + + public function call(Context $context, JITVariable ...$args): Value + { + throw new \LogicException( + 'sem_remove() is not supported for JIT/AOT in this compiler build (issue #3704)' + ); + } + + private function triggerWarning(Frame $frame, string $message): void + { + if (null === $frame->vmContext) { + return; + } + $frame->vmContext->errors->triggerError( + $message, + ErrorReporter::E_WARNING, + '' !== $frame->scriptPath ? $frame->scriptPath : null, + $frame->vmContext, + $frame + ); + } +} diff --git a/lib/Runtime.php b/lib/Runtime.php index aa85e5f1be1..9ecf784f705 100755 --- a/lib/Runtime.php +++ b/lib/Runtime.php @@ -271,6 +271,7 @@ private function loadCoreModules(): void { $this->load(new ext\uuid\Module); $this->load(new ext\uploadprogress\Module); $this->load(new ext\sysvshm\Module); + $this->load(new ext\sysvsem\Module); $this->load(new ext\standard\Module); } diff --git a/script/capability-matrix.php b/script/capability-matrix.php index 0bf6dc46f2e..8ca12b94585 100755 --- a/script/capability-matrix.php +++ b/script/capability-matrix.php @@ -44,6 +44,7 @@ function collectCapabilities(string $root): array 'uuid' => new PHPCompiler\ext\uuid\Module(), 'uploadprogress' => new PHPCompiler\ext\uploadprogress\Module(), 'sysvshm' => new PHPCompiler\ext\sysvshm\Module(), + 'sysvsem' => new PHPCompiler\ext\sysvsem\Module(), ]; $capabilities = []; diff --git a/test/compliance/cases/stdlib/sem_acquire_release.phpt b/test/compliance/cases/stdlib/sem_acquire_release.phpt new file mode 100644 index 00000000000..6d10f166677 --- /dev/null +++ b/test/compliance/cases/stdlib/sem_acquire_release.phpt @@ -0,0 +1,25 @@ +--TEST-- +stdlib sem_get/acquire/release round-trip (#3704) +--SKIPIF-- + +--FILE-- + +--EXPECT-- +fn +SysvSemaphore +acq +rel +done diff --git a/test/unit/SysvSemBuiltinTest.php b/test/unit/SysvSemBuiltinTest.php new file mode 100644 index 00000000000..71f7f5585ee --- /dev/null +++ b/test/unit/SysvSemBuiltinTest.php @@ -0,0 +1,28 @@ +BIN = realpath(__DIR__.'/../../bin/vm.php'); + } + + public static function providePHPTests(): \Generator + { + if (!\function_exists('sem_get')) { + return; + } + + $path = __DIR__.'/../compliance/cases/stdlib/sem_acquire_release.phpt'; + yield 'sem_acquire_release.phpt' => self::parsePHPT($path, 'sem_acquire_release.phpt'); + } +}