From 09fae1dacf58755fe4dc87b5370503aac8ff146c 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 --- CHANGELOG.md | 5 + Caster/FFICaster.php | 161 +++++++++++ Cloner/AbstractCloner.php | 3 + Tests/Caster/FFICasterTest.php | 471 +++++++++++++++++++++++++++++++++ 4 files changed, 640 insertions(+) create mode 100644 Caster/FFICaster.php create mode 100644 Tests/Caster/FFICasterTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f58ed317..a9ea31d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.2 +--- + + * Add support for `FFI\CData` and `FFI\CType` + 5.4 --- diff --git a/Caster/FFICaster.php b/Caster/FFICaster.php new file mode 100644 index 00000000..7d90e7b5 --- /dev/null +++ b/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/Cloner/AbstractCloner.php b/Cloner/AbstractCloner.php index 636c39ee..b2aaa5da 100644 --- a/Cloner/AbstractCloner.php +++ b/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/Tests/Caster/FFICasterTest.php b/Tests/Caster/FFICasterTest.php new file mode 100644 index 00000000..fc751c79 --- /dev/null +++ b/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); + } +}