From 1c7dc52abb07f3ad40500ac71a43ce3228be0e48 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 25 Jun 2022 03:35:26 +0300 Subject: [PATCH] [VarDumper] Add `FFI\CData` and `FFI\CType` types --- src/Symfony/Component/VarDumper/CHANGELOG.md | 5 + .../Component/VarDumper/Caster/FFICaster.php | 161 ++++++ .../VarDumper/Cloner/AbstractCloner.php | 3 + .../VarDumper/Tests/Caster/FFICasterTest.php | 471 ++++++++++++++++++ 4 files changed, 640 insertions(+) create mode 100644 src/Symfony/Component/VarDumper/Caster/FFICaster.php create mode 100644 src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php diff --git a/src/Symfony/Component/VarDumper/CHANGELOG.md b/src/Symfony/Component/VarDumper/CHANGELOG.md index f58ed31706084..a9ea31d364da4 100644 --- a/src/Symfony/Component/VarDumper/CHANGELOG.md +++ b/src/Symfony/Component/VarDumper/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.2 +--- + + * Add support for `FFI\CData` and `FFI\CType` + 5.4 --- diff --git a/src/Symfony/Component/VarDumper/Caster/FFICaster.php b/src/Symfony/Component/VarDumper/Caster/FFICaster.php new file mode 100644 index 0000000000000..7d90e7b57fac8 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Caster/FFICaster.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Caster; + +use FFI\CData; +use FFI\CType; +use Symfony\Component\VarDumper\Cloner\Stub; + +/** + * Casts FFI extension classes to array representation. + * + * @author Nesmeyanov Kirill + */ +final class FFICaster +{ + /** + * In case of "char*" contains a string, the length of which depends on + * some other parameter, then during the generation of the string it is + * possible to go beyond the allowable memory area. + * + * This restriction serves to ensure that processing does not take + * up the entire allowable PHP memory limit. + */ + private const MAX_STRING_LENGTH = 255; + + public static function castCTypeOrCData(CData|CType $data, array $args, Stub $stub): array + { + if ($data instanceof CType) { + $type = $data; + $data = null; + } else { + $type = \FFI::typeof($data); + } + + $stub->class = sprintf('%s<%s> size %d align %d', ($data ?? $type)::class, $type->getName(), $type->getSize(), $type->getAlignment()); + + return match ($type->getKind()) { + CType::TYPE_FLOAT, + CType::TYPE_DOUBLE, + \defined('\FFI\CType::TYPE_LONGDOUBLE') ? CType::TYPE_LONGDOUBLE : -1, + CType::TYPE_UINT8, + CType::TYPE_SINT8, + CType::TYPE_UINT16, + CType::TYPE_SINT16, + CType::TYPE_UINT32, + CType::TYPE_SINT32, + CType::TYPE_UINT64, + CType::TYPE_SINT64, + CType::TYPE_BOOL, + CType::TYPE_CHAR, + CType::TYPE_ENUM => null !== $data ? [Caster::PREFIX_VIRTUAL.'cdata' => $data->cdata] : [], + CType::TYPE_POINTER => self::castFFIPointer($stub, $type, $data), + CType::TYPE_STRUCT => self::castFFIStructLike($type, $data), + CType::TYPE_FUNC => self::castFFIFunction($stub, $type), + default => $args, + }; + } + + private static function castFFIFunction(Stub $stub, CType $type): array + { + $arguments = []; + + for ($i = 0, $count = $type->getFuncParameterCount(); $i < $count; ++$i) { + $param = $type->getFuncParameterType($i); + + $arguments[] = $param->getName(); + } + + $abi = match ($type->getFuncABI()) { + CType::ABI_DEFAULT, + CType::ABI_CDECL => '[cdecl]', + CType::ABI_FASTCALL => '[fastcall]', + CType::ABI_THISCALL => '[thiscall]', + CType::ABI_STDCALL => '[stdcall]', + CType::ABI_PASCAL => '[pascal]', + CType::ABI_REGISTER => '[register]', + CType::ABI_MS => '[ms]', + CType::ABI_SYSV => '[sysv]', + CType::ABI_VECTORCALL => '[vectorcall]', + default => '[unknown abi]' + }; + + $returnType = $type->getFuncReturnType(); + + $stub->class = $abi.' callable('.implode(', ', $arguments).'): ' + .$returnType->getName(); + + return [Caster::PREFIX_VIRTUAL.'returnType' => $returnType]; + } + + private static function castFFIPointer(Stub $stub, CType $type, CData $data = null): array + { + $ptr = $type->getPointerType(); + + if (null === $data) { + return [Caster::PREFIX_VIRTUAL.'0' => $ptr]; + } + + return match ($ptr->getKind()) { + CType::TYPE_CHAR => [Caster::PREFIX_VIRTUAL.'cdata' => self::castFFIStringValue($data)], + CType::TYPE_FUNC => self::castFFIFunction($stub, $ptr), + default => [Caster::PREFIX_VIRTUAL.'cdata' => $data[0]], + }; + } + + private static function castFFIStringValue(CData $data): string|CutStub + { + $result = []; + + for ($i = 0; $i < self::MAX_STRING_LENGTH; ++$i) { + $result[$i] = $data[$i]; + + if ("\0" === $result[$i]) { + return implode('', $result); + } + } + + $string = implode('', $result); + $stub = new CutStub($string); + $stub->cut = -1; + $stub->value = $string; + + return $stub; + } + + private static function castFFIStructLike(CType $type, CData $data = null): array + { + $isUnion = ($type->getAttributes() & CType::ATTR_UNION) === CType::ATTR_UNION; + + $result = []; + + foreach ($type->getStructFieldNames() as $name) { + $field = $type->getStructFieldType($name); + + // Retrieving the value of a field from a union containing + // a pointer is not a safe operation, because may contain + // incorrect data. + $isUnsafe = $isUnion && CType::TYPE_POINTER === $field->getKind(); + + if ($isUnsafe) { + $result[Caster::PREFIX_VIRTUAL.$name.'?'] = $field; + } elseif (null === $data) { + $result[Caster::PREFIX_VIRTUAL.$name] = $field; + } else { + $fieldName = $data->{$name} instanceof CData ? '' : $field->getName().' '; + $result[Caster::PREFIX_VIRTUAL.$fieldName.$name] = $data->{$name}; + } + } + + return $result; + } +} diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index 636c39ee2b711..b2aaa5daecc02 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -187,6 +187,9 @@ abstract class AbstractCloner implements ClonerInterface 'RdKafka\Topic' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castTopic'], 'RdKafka\TopicPartition' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castTopicPartition'], 'RdKafka\TopicConf' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castTopicConf'], + + 'FFI\CData' => ['Symfony\Component\VarDumper\Caster\FFICaster', 'castCTypeOrCData'], + 'FFI\CType' => ['Symfony\Component\VarDumper\Caster\FFICaster', 'castCTypeOrCData'], ]; protected $maxItems = 2500; diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php new file mode 100644 index 0000000000000..fc751c79edbb9 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php @@ -0,0 +1,471 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests\Caster; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\VarDumper\Caster\FFICaster; +use Symfony\Component\VarDumper\Test\VarDumperTestTrait; + +/** + * @author Kirill Nesmeyanov + * + * @requires extension ffi + */ +class FFICasterTest extends TestCase +{ + use VarDumperTestTrait; + + protected function setUp(): void + { + if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && 'preload' === \ini_get('ffi.enable')) { + return; + } + if (!filter_var(\ini_get('ffi.enable'), \FILTER_VALIDATE_BOOLEAN)) { + $this->markTestSkipped('FFI not enabled for CLI SAPI'); + } + } + + public function testCastAnonymousStruct() + { + $this->assertDumpEquals(<<<'PHP' + FFI\CData> size 4 align 4 { + uint32_t x: 0 + } + PHP, \FFI::new('struct { uint32_t x; }')); + } + + public function testCastNamedStruct() + { + $this->assertDumpEquals(<<<'PHP' + FFI\CData size 4 align 4 { + uint32_t x: 0 + } + PHP, \FFI::new('struct Example { uint32_t x; }')); + } + + public function testCastAnonymousUnion() + { + $this->assertDumpEquals(<<<'PHP' + FFI\CData> size 4 align 4 { + uint32_t x: 0 + uint32_t y: 0 + } + PHP, \FFI::new('union { uint32_t x; uint32_t y; }')); + } + + public function testCastNamedUnion() + { + $this->assertDumpEquals(<<<'PHP' + FFI\CData size 4 align 4 { + uint32_t x: 0 + uint32_t y: 0 + } + PHP, \FFI::new('union Example { uint32_t x; uint32_t y; }')); + } + + public function testCastAnonymousEnum() + { + $this->assertDumpEquals(<<<'PHP' + FFI\CData> size 4 align 4 { + cdata: 0 + } + PHP, \FFI::new('enum { a, b }')); + } + + public function testCastNamedEnum() + { + $this->assertDumpEquals(<<<'PHP' + FFI\CData size 4 align 4 { + cdata: 0 + } + PHP, \FFI::new('enum Example { a, b }')); + } + + public function scalarsDataProvider(): array + { + return [ + 'int8_t' => ['int8_t', '0', 1, 1], + 'uint8_t' => ['uint8_t', '0', 1, 1], + 'int16_t' => ['int16_t', '0', 2, 2], + 'uint16_t' => ['uint16_t', '0', 2, 2], + 'int32_t' => ['int32_t', '0', 4, 4], + 'uint32_t' => ['uint32_t', '0', 4, 4], + 'int64_t' => ['int64_t', '0', 8, 8], + 'uint64_t' => ['uint64_t', '0', 8, 8], + + 'bool' => ['bool', 'false', 1, 1], + 'char' => ['char', '"\x00"', 1, 1], + 'float' => ['float', '0.0', 4, 4], + 'double' => ['double', '0.0', 8, 8], + ]; + } + + /** + * @dataProvider scalarsDataProvider + */ + public function testCastScalar(string $type, string $value, int $size, int $align) + { + $this->assertDumpEquals(<< size $size align $align { + cdata: $value + } + PHP, \FFI::new($type)); + } + + public function testCastVoidFunction() + { + $abi = \PHP_OS_FAMILY === 'Windows' ? '[cdecl]' : '[fastcall]'; + + $this->assertDumpEquals(<< size 1 align 1 {} + } + PHP, \FFI::new('void (*)(void)')); + } + + public function testCastIntFunction() + { + $abi = \PHP_OS_FAMILY === 'Windows' ? '[cdecl]' : '[fastcall]'; + + $this->assertDumpEquals(<< size 8 align 8 {} + } + PHP, \FFI::new('unsigned long long (*)(void)')); + } + + public function testCastFunctionWithArguments() + { + $abi = \PHP_OS_FAMILY === 'Windows' ? '[cdecl]' : '[fastcall]'; + + $this->assertDumpEquals(<< size 1 align 1 {} + } + PHP, \FFI::new('void (*)(int a, const char* b)')); + } + + public function testCastNonCuttedPointerToChar() + { + $actualMessage = "Hello World!\0"; + + $string = \FFI::new('char[100]'); + $pointer = \FFI::addr($string[0]); + \FFI::memcpy($pointer, $actualMessage, \strlen($actualMessage)); + + $this->assertDumpEquals(<<<'PHP' + FFI\CData size 8 align 8 { + cdata: "Hello World!\x00" + } + PHP, $pointer); + } + + public function testCastCuttedPointerToChar() + { + $actualMessage = str_repeat('Hello World!', 30)."\0"; + $actualLength = \strlen($actualMessage); + + $expectedMessage = 'Hello World!Hello World!Hello World!Hello World!' + .'Hello World!Hello World!Hello World!Hello World!Hello World!Hel' + .'lo World!Hello World!Hello World!Hello World!Hello World!Hello ' + .'World!Hello World!Hello World!Hello World!Hello World!Hello Wor' + .'ld!Hello World!Hel'; + + $string = \FFI::new('char['.$actualLength.']'); + $pointer = \FFI::addr($string[0]); + \FFI::memcpy($pointer, $actualMessage, $actualLength); + + $this->assertDumpEquals(<< size 8 align 8 { + cdata: "$expectedMessage"… + } + PHP, $pointer); + } + + /** + * It is worth noting that such a test can cause SIGSEGV, as it breaks + * into "foreign" memory. However, this is only theoretical, since + * memory is allocated within the PHP process and almost always "garbage + * data" will be read from the PHP process itself. + * + * If this test fails for some reason, please report it: We may have to + * disable the dumping of strings ("char*") feature in VarDumper. + * + * @see FFICaster::castFFIStringValue() + */ + public function testCastNonTrailingCharPointer() + { + $actualMessage = 'Hello World!'; + $actualLength = \strlen($actualMessage); + + $string = \FFI::new('char['.$actualLength.']'); + $pointer = \FFI::addr($string[0]); + + \FFI::memcpy($pointer, $actualMessage, $actualLength); + + // Remove automatically addition of the trailing "\0" and remove trailing "\0" + $pointer = \FFI::cast('char*', \FFI::cast('void*', $pointer)); + $pointer[$actualLength] = "\x01"; + + $this->assertDumpMatchesFormat(<< size 8 align 8 { + cdata: "$actualMessage%s" + } + PHP, $pointer); + } + + public function testCastUnionWithDirectReferencedFields() + { + $ffi = \FFI::cdef(<<<'CPP' + typedef union Event { + int32_t x; + float y; + } Event; + CPP); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData size 4 align 4 { + int32_t x: 0 + float y: 0.0 + } + OUTPUT, $ffi->new('Event')); + } + + public function testCastUnionWithPointerReferencedFields() + { + $ffi = \FFI::cdef(<<<'CPP' + typedef union Event { + void* something; + char* string; + } Event; + CPP); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData size 8 align 8 { + something?: FFI\CType size 8 align 8 { + 0: FFI\CType size 1 align 1 {} + } + string?: FFI\CType size 8 align 8 { + 0: FFI\CType size 1 align 1 {} + } + } + OUTPUT, $ffi->new('Event')); + } + + public function testCastUnionWithMixedFields() + { + $ffi = \FFI::cdef(<<<'CPP' + typedef union Event { + void* a; + int32_t b; + char* c; + ptrdiff_t d; + } Event; + CPP); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData size 8 align 8 { + a?: FFI\CType size 8 align 8 { + 0: FFI\CType size 1 align 1 {} + } + int32_t b: 0 + c?: FFI\CType size 8 align 8 { + 0: FFI\CType size 1 align 1 {} + } + int64_t d: 0 + } + OUTPUT, $ffi->new('Event')); + } + + public function testCastPointerToEmptyScalars() + { + $ffi = \FFI::cdef(<<<'CPP' + typedef struct { + int8_t *a; + uint8_t *b; + int64_t *c; + uint64_t *d; + float *e; + double *f; + bool *g; + } Example; + CPP); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData> size 56 align 8 { + int8_t* a: null + uint8_t* b: null + int64_t* c: null + uint64_t* d: null + float* e: null + double* f: null + bool* g: null + } + OUTPUT, $ffi->new('Example')); + } + + public function testCastPointerToNonEmptyScalars() + { + $ffi = \FFI::cdef(<<<'CPP' + typedef struct { + int8_t *a; + uint8_t *b; + int64_t *c; + uint64_t *d; + float *e; + double *f; + bool *g; + } Example; + CPP); + + // Create values + $int = \FFI::new('int64_t'); + $int->cdata = 42; + $float = \FFI::new('float'); + $float->cdata = 42.0; + $double = \FFI::new('double'); + $double->cdata = 42.2; + $bool = \FFI::new('bool'); + $bool->cdata = true; + + // Fill struct + $struct = $ffi->new('Example'); + $struct->a = \FFI::addr(\FFI::cast('int8_t', $int)); + $struct->b = \FFI::addr(\FFI::cast('uint8_t', $int)); + $struct->c = \FFI::addr(\FFI::cast('int64_t', $int)); + $struct->d = \FFI::addr(\FFI::cast('uint64_t', $int)); + $struct->e = \FFI::addr(\FFI::cast('float', $float)); + $struct->f = \FFI::addr(\FFI::cast('double', $double)); + $struct->g = \FFI::addr(\FFI::cast('bool', $bool)); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData> size 56 align 8 { + a: FFI\CData size 8 align 8 { + cdata: 42 + } + b: FFI\CData size 8 align 8 { + cdata: 42 + } + c: FFI\CData size 8 align 8 { + cdata: 42 + } + d: FFI\CData size 8 align 8 { + cdata: 42 + } + e: FFI\CData size 8 align 8 { + cdata: 42.0 + } + f: FFI\CData size 8 align 8 { + cdata: 42.2 + } + g: FFI\CData size 8 align 8 { + cdata: true + } + } + OUTPUT, $struct); + } + + public function testCastPointerToStruct() + { + $ffi = \FFI::cdef(<<<'CPP' + typedef struct { + int8_t a; + } Example; + CPP); + + $struct = $ffi->new('Example', false); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData*> size 8 align 8 { + cdata: FFI\CData> size 1 align 1 { + int8_t a: 0 + } + } + OUTPUT, \FFI::addr($struct)); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData**> size 8 align 8 { + cdata: null + } + OUTPUT, \FFI::addr(\FFI::addr($struct))); + + // Save the pointer as variable so that + // it is not cleaned up by the GC + $pointer = \FFI::addr($struct); + + $this->assertDumpEquals(<<<'OUTPUT' + FFI\CData**> size 8 align 8 { + cdata: FFI\CData*> size 8 align 8 { + cdata: FFI\CData> size 1 align 1 { + int8_t a: 0 + } + } + } + OUTPUT, \FFI::addr($pointer)); + } + + public function testCastComplexType() + { + $ffi = \FFI::cdef(<<<'CPP' + typedef struct { + int x; + int y; + } Point; + typedef struct Example { + uint8_t a[32]; + long b; + __extension__ union { + __extension__ struct { + short c; + long d; + }; + struct { + Point point; + float e; + }; + }; + short f; + bool g; + int (*func)( + struct __sub *h + ); + } Example; + CPP); + + $var = $ffi->new('Example'); + $var->func = (static fn (object $p) => 42); + + $abi = \PHP_OS_FAMILY === 'Windows' ? '[cdecl]' : '[fastcall]'; + $longSize = \FFI::type('long')->getSize(); + $longType = 8 === $longSize ? 'int64_t' : 'int32_t'; + $structSize = 56 + $longSize * 2; + + $this->assertDumpEquals(<< size $structSize align 8 { + a: FFI\CData size 32 align 1 {} + $longType b: 0 + int16_t c: 0 + $longType d: 0 + point: FFI\CData> size 8 align 4 { + int32_t x: 0 + int32_t y: 0 + } + float e: 0.0 + int16_t f: 0 + bool g: false + func: $abi callable(struct __sub*): int32_t { + returnType: FFI\CType size 4 align 4 {} + } + } + OUTPUT, $var); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy