Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/sockets/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function getFunctions(): array
new socket_sendto(),
new socket_recvfrom(),
new socket_shutdown(),
new socket_select(),
new socket_read(),
new socket_write(),
new socket_close(),
Expand Down
41 changes: 41 additions & 0 deletions ext/sockets/SocketsLibcThinAbi.php
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,41 @@ public static function close(int $fd): int
return (int) $ffi->close($fd);
}

/**
* poll(2) — thin ABI for socket_select() (#6395).
*
* @param list<array{fd: int, events: int}> $entries
*
* @return list<int>|false revents per entry, or false on error
*/
public static function poll(array $entries, int $timeoutMs): array|false
{
$ffi = self::ffi();
if (null === $ffi) {
return false;
}
$n = \count($entries);
if (0 === $n) {
return [];
}
$pollfd = $ffi->new('struct pollfd['.$n.']');
for ($i = 0; $i < $n; ++$i) {
$pollfd[$i]->fd = $entries[$i]['fd'];
$pollfd[$i]->events = $entries[$i]['events'];
$pollfd[$i]->revents = 0;
}
$rc = (int) $ffi->poll($pollfd, $n, $timeoutMs);
if ($rc < 0) {
return false;
}
$out = [];
for ($i = 0; $i < $n; ++$i) {
$out[] = (int) $pollfd[$i]->revents;
}

return $out;
}

public static function fcntlGetFl(int $fd): int
{
$ffi = self::ffi();
Expand Down Expand Up @@ -494,6 +529,12 @@ private static function ffi(): ?\FFI
int fcntl(int fd, int cmd, ...);
int sockatmark(int sockfd);
int shutdown(int sockfd, int how);
struct pollfd {
int fd;
short events;
short revents;
};
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, unsigned int size);
unsigned short htons(unsigned short hostshort);
Expand Down
102 changes: 102 additions & 0 deletions ext/sockets/VmSockets.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,108 @@ public static function atmarkForFd(int $fd): bool
return $r >= 0 && 0 !== $r;
}

/**
* socket_select() via poll(2) (php-src ext/sockets/sockets.c; #6395).
*
* @param list<array{key: int|string, object: ObjectEntry, fd: int}>|null $read
* @param list<array{key: int|string, object: ObjectEntry, fd: int}>|null $write
* @param list<array{key: int|string, object: ObjectEntry, fd: int}>|null $except
*
* @return array{read: list<array{key: int|string, object: ObjectEntry, fd: int}>, write: list<array{key: int|string, object: ObjectEntry, fd: int}>, except: list<array{key: int|string, object: ObjectEntry, fd: int}>, count: int}|false
*/
public static function select(
?array $read,
?array $write,
?array $except,
int $seconds,
int $microseconds,
): array|false {
$polLin = 0x001;
$polLout = 0x004;
$polLerr = 0x008;
$polLhup = 0x010;
$polLpri = 0x002;

/** @var list<array{slot: array{key: int|string, object: ObjectEntry, fd: int}, events: int, kind: int}> $entries */
$entries = [];
if (null !== $read) {
foreach ($read as $slot) {
$entries[] = ['slot' => $slot, 'events' => $polLin | $polLhup, 'kind' => 1];
}
}
if (null !== $write) {
foreach ($write as $slot) {
$entries[] = ['slot' => $slot, 'events' => $polLout, 'kind' => 2];
}
}
if (null !== $except) {
foreach ($except as $slot) {
$entries[] = ['slot' => $slot, 'events' => $polLerr | $polLhup | $polLpri, 'kind' => 3];
}
}

$timeoutMs = -1;
if ($seconds >= 0) {
$timeoutMs = ($seconds * 1000) + (int) \floor($microseconds / 1000);
if ($timeoutMs < 0) {
$timeoutMs = 0;
}
}

if ([] === $entries) {
// No descriptors but at least one empty array was passed — timeout only.
if ($timeoutMs > 0) {
usleep($timeoutMs * 1000);
}

return [
'read' => [],
'write' => [],
'except' => [],
'count' => 0,
];
}

$pollEntries = [];
foreach ($entries as $entry) {
$pollEntries[] = ['fd' => $entry['slot']['fd'], 'events' => $entry['events']];
}
$revents = SocketsLibcThinAbi::poll($pollEntries, $timeoutMs);
if (false === $revents) {
self::recordError(null, SocketsLibcThinAbi::readErrno());

return false;
}

$readyRead = [];
$readyWrite = [];
$readyExcept = [];
$readyCount = 0;
foreach ($revents as $i => $rev) {
if (0 === $rev) {
continue;
}
$entry = $entries[$i];
$requested = $entry['events'];
if (0 === ($rev & $requested) && 0 === ($rev & ($polLerr | $polLhup))) {
continue;
}
match ($entry['kind']) {
1 => $readyRead[] = $entry['slot'],
2 => $readyWrite[] = $entry['slot'],
3 => $readyExcept[] = $entry['slot'],
};
++$readyCount;
}

return [
'read' => $readyRead,
'write' => $readyWrite,
'except' => $readyExcept,
'count' => $readyCount,
];
}

/** php-src: ext/sockets/sockets.c — PHP_FUNCTION(socket_set_nonblock) via fcntl(F_SETFL). */
public static function setNonblockForObject(ObjectEntry $object): bool
{
Expand Down
174 changes: 174 additions & 0 deletions ext/sockets/socket_select.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\sockets;

use PHPCompiler\Frame;
use PHPCompiler\Func\Internal;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPCompiler\VM\BuiltinExecute;
use PHPCompiler\VM\HashTable;
use PHPCompiler\VM\Variable;
use PHPLLVM\Value;

/**
* socket_select() — multiplex Socket objects via poll(2) (php-src ext/sockets/sockets.c; #6395).
*
* VM-first; JIT deferred like sister set_block/set_nonblock builtins.
*
* @see https://github.com/php/php-src/blob/master/ext/sockets/sockets.c PHP_FUNCTION(socket_select)
*/
final class socket_select extends Internal
{
public function __construct()
{
parent::__construct('socket_select');
}

public function execute(Frame $frame): void
{
$argc = \count($frame->calledArgs);
if ($argc < 4) {
throw new \ArgumentCountError(
'socket_select() expects at least 4 arguments, '.$argc.' given'
);
}
if ($argc > 5) {
throw new \ArgumentCountError(
'socket_select() expects at most 5 arguments, '.$argc.' given'
);
}

$readSlots = self::parseSocketArrayArg($frame->calledArgs[0], 1, 'read');
$writeSlots = self::parseSocketArrayArg($frame->calledArgs[1], 2, 'write');
$exceptSlots = self::parseSocketArrayArg($frame->calledArgs[2], 3, 'except');

if (null === $readSlots && null === $writeSlots && null === $exceptSlots) {
throw new \ValueError('socket_select(): At least one array argument must be passed');
}

$seconds = VmSocketArg::requireIntArg($frame->calledArgs[3], 'socket_select', 4, 'seconds');
$microseconds = 0;
if ($argc >= 5) {
$microseconds = VmSocketArg::requireIntArg(
$frame->calledArgs[4],
'socket_select',
5,
'microseconds'
);
}

$result = VmSockets::select(
$readSlots,
$writeSlots,
$exceptSlots,
$seconds,
$microseconds
);
if (false === $result) {
VmSockets::triggerWarning($frame, 'socket_select(): unable to select');
BuiltinExecute::writeReturn(
$frame,
static fn (Variable $ret) => $ret->bool(false)
);

return;
}

if (null !== $readSlots) {
self::writeBackSocketArray($frame->calledArgs[0], $result['read']);
}
if (null !== $writeSlots) {
self::writeBackSocketArray($frame->calledArgs[1], $result['write']);
}
if (null !== $exceptSlots) {
self::writeBackSocketArray($frame->calledArgs[2], $result['except']);
}

BuiltinExecute::writeReturn(
$frame,
static fn (Variable $ret) => $ret->int($result['count'])
);
}

public function call(Context $context, JITVariable ...$args): Value
{
throw new \LogicException('socket_select() JIT lowering not implemented (#6395)');
}

/**
* @return list<array{key: int|string, object: \PHPCompiler\VM\ObjectEntry, fd: int}>|null
*/
private static function parseSocketArrayArg(Variable $arg, int $argNum, string $paramName): ?array
{
$arg = $arg->resolveIndirect();
if (Variable::TYPE_NULL === $arg->type) {
return null;
}
if (Variable::TYPE_ARRAY !== $arg->type) {
throw new \TypeError(\sprintf(
'socket_select(): Argument #%d ($%s) must be of type ?array, %s given',
$argNum,
$paramName,
\PHPCompiler\ext\standard\VmStreamArg::debugTypeName($arg)
));
}

$slots = [];
foreach ($arg->toArray()->iterateKeyed(true) as [$keyVar, $value]) {
$value = $value->resolveIndirect();
try {
$object = VmSocketArg::requireSocketObject($value, 'socket_select', $argNum);
} catch (\TypeError) {
$given = \PHPCompiler\ext\standard\VmStreamArg::debugTypeName($value);
throw new \TypeError(\sprintf(
'socket_select(): Argument #%d ($%s) must only have elements of type Socket, %s given',
$argNum,
$paramName,
$given
));
}
$fd = VmSocket::fdForObject($object);
if (null === $fd) {
throw new \TypeError(
'socket_select(): supplied resource is not a valid Socket resource'
);
}
$keyVar = $keyVar->resolveIndirect();
$key = Variable::TYPE_INTEGER === $keyVar->type
? $keyVar->toInt()
: $keyVar->toString();
$slots[] = [
'key' => $key,
'object' => $object,
'fd' => $fd,
];
}

return $slots;
}

/**
* @param list<array{key: int|string, object: \PHPCompiler\VM\ObjectEntry, fd: int}> $ready
*/
private static function writeBackSocketArray(Variable $targetVar, array $ready): void
{
$targetVar = $targetVar->resolveIndirect();
$ht = new HashTable();
foreach ($ready as $slot) {
$cell = new Variable();
$cell->object($slot['object']);
$key = $slot['key'];
if (\is_int($key)) {
$ht->addIndex($key, $cell);
} else {
$ht->add((string) $key, $cell);
}
}
$replacement = new Variable();
$replacement->array($ht);
$targetVar->copyFrom($replacement);
}
}
2 changes: 2 additions & 0 deletions lib/BuiltinByRefParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public static function forFunction(string $name): array
return [1, 2];
case 'socket_create_pair':
return [3];
case 'socket_select':
return [0, 1, 2];
case 'socket_getsockname':
case 'socket_getpeername':
return [1, 2];
Expand Down
2 changes: 2 additions & 0 deletions lib/BuiltinParamNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ public static function forFunction(string $name): ?array
case 'fwrite':
case 'fputs':
return ['stream', 'data', 'length'];
case 'socket_select':
return ['read', 'write', 'except', 'seconds', 'microseconds'];
case 'fputcsv':
return ['stream', 'fields', 'separator', 'enclosure', 'escape', 'eol'];
case 'stream_context_create':
Expand Down
Loading