Skip to content

Commit 75ade6a

Browse files
authored
Merge pull request #12 from lisachenko/feature/ffi-preload
Implement definition preloading with FFI preload mode
2 parents 71d5862 + e29ffab commit 75ade6a

15 files changed

+240
-58
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ Pre-requisites and initialization
2424
--------------
2525

2626
As this library depends on `FFI`, it requires PHP>=7.4 and `FFI` extension to be enabled.
27-
It should work in CLI mode without any troubles, whereas for web mode `preload` mode should be implemented (not done yet), so please configure `ffi.enable` to be `true`.
27+
It should work in CLI mode without any troubles, whereas for web mode `preload` mode should be activated.
2828
Also, current version is limited to x64 non-thread-safe versions of PHP.
2929

3030
To install this library, simply add it via `composer`:
3131
```shell script
3232
composer require lisachenko/z-engine
3333
```
34+
To activate a `preload` mode, please add `Core::preload()` call into your script, specified by `opcache.preload`. This call will be done during the server preload and will be used by library to bypass unnecessary C headers processing during each request.
3435

3536
Next step is to init library itself with short call to the `Core::init()`:
3637
```php

include/engine_x64_nts.h

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#define FFI_SCOPE "ZEngine"
2+
#define FFI_LIB "ZEND_LIBRARY_NAME"
3+
14
typedef int64_t zend_long;
25
typedef uint64_t zend_ulong;
36
typedef int64_t zend_off_t;

preload.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
/**
3+
* Z-Engine framework
4+
*
5+
* @copyright Copyright 2019, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
declare(strict_types=1);
11+
12+
include __DIR__.'/vendor/autoload.php';
13+
14+
use ZEngine\Core;
15+
16+
/**
17+
* This file should be loaded during the preload stage, which is defined by opcache.preload file.
18+
* Either include it manually, or just add following line into your init section.
19+
*/
20+
Core::preload();
21+

src/Core.php

+78-14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use FFI;
1616
use FFI\CData;
1717
use FFI\CType;
18+
use ZEngine\Macro\DefinitionLoader;
1819
use ZEngine\System\Compiler;
1920
use ZEngine\System\Executor;
2021

@@ -222,28 +223,44 @@ public static function init()
222223
throw new \RuntimeException('Only x64 non thread-safe versions of PHP are supported');
223224
}
224225

225-
$definition = file_get_contents(__DIR__ . '/../include/engine_x64_nts.h');
226-
$macros = [
227-
'ZEND_API' => '__declspec(dllimport)',
228-
'ZEND_FASTCALL' => $isWindowsPlatform ? '__vectorcall' : '',
226+
try {
227+
$engine = FFI::scope('ZEngine');
228+
} catch (FFI\Exception $e) {
229+
if (ini_get('ffi.enable') === 'preload' && PHP_SAPI !== 'cli') {
230+
throw new \RuntimeException('Preload mode requires that you call Core::preload before');
231+
}
232+
// If not, then load definitions by hand
233+
$definition = file_get_contents(DefinitionLoader::wrap(__DIR__.'/../include/engine_x64_nts.h'));
234+
$arguments = [$definition];
229235

230-
'ZEND_MAX_RESERVED_RESOURCES' => '6'
231-
];
236+
// For Windows platform we should load symbols from the shared php7.dll library
237+
if ($isWindowsPlatform) {
238+
$arguments[] = 'php7.dll';
239+
}
232240

233-
// Simple macros resolving
234-
$definition = strtr($definition, $macros);
235-
$arguments = [$definition];
236-
237-
// For Windows platform we should load symbols from the shared php7.dll library
238-
if ($isWindowsPlatform) {
239-
$arguments[] = 'php7.dll';
241+
$engine = FFI::cdef(...$arguments);
240242
}
241-
self::$engine = $engine = FFI::cdef(...$arguments);
243+
self::$engine = $engine;
244+
242245
assert(!$isThreadSafe, 'Following properties available only for non thread-safe version');
243246
self::$executor = new Executor($engine->executor_globals);
244247
self::$compiler = new Compiler($engine->compiler_globals);
245248
}
246249

250+
/**
251+
* Preloads definition and Core for ffi.preload mode, should be called during preload stage for better performance
252+
*/
253+
public static function preload()
254+
{
255+
$definition = file_get_contents(DefinitionLoader::wrap(__DIR__.'/../include/engine_x64_nts.h'));
256+
$tempFile = tempnam(sys_get_temp_dir(), 'php_ffi');
257+
file_put_contents($tempFile, $definition);
258+
FFI::load($tempFile);
259+
260+
// Performs initialization of properties, otherwise we will get an error about uninitialized properties
261+
Core::init();
262+
}
263+
247264
/**
248265
* Internally cast a memory at given pointer to another type
249266
*/
@@ -252,6 +269,45 @@ public static function cast(string $type, CData $pointer): CData
252269
return self::$engine->cast($type, $pointer);
253270
}
254271

272+
/**
273+
* Returns the size of given type
274+
*/
275+
public static function sizeof($cType): int
276+
{
277+
return FFI::sizeof($cType);
278+
}
279+
280+
/**
281+
* Returns the size of given type
282+
*/
283+
public static function addr(CData $variable): CData
284+
{
285+
return FFI::addr($variable);
286+
}
287+
288+
/**
289+
* Copies $size bytes from memory area $source to memory area $target.
290+
* $source may be any native data structure (FFI\CData) or PHP string.
291+
*
292+
* @param CData $target
293+
* @param mixed $source
294+
* @param int $size
295+
*/
296+
public static function memcpy(CData &$target, &$source, int $size): void
297+
{
298+
FFI::memcpy($target, $source, $size);
299+
}
300+
301+
/**
302+
* Creates a PHP string from $size bytes of memory area pointed by
303+
* $source. If size is omitted, $source must be zero terminated
304+
* array of C chars.
305+
*/
306+
public static function string(CData $source, int $size = 0): string
307+
{
308+
return FFI::string($source, $size);
309+
}
310+
255311
/**
256312
* Creates a new instance of specific type
257313
*
@@ -262,6 +318,14 @@ public static function new(string $type, bool $owned = true, bool $persistent =
262318
return self::$engine->new($type, $owned, $persistent);
263319
}
264320

321+
/**
322+
* Returns the size of given type
323+
*/
324+
public static function free(CData $variable): void
325+
{
326+
self::$engine->free($variable);
327+
}
328+
265329
/**
266330
* Returns a CType definition for engine by type name
267331
*

src/Macro/DefinitionLoader.php

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
/**
3+
* Z-Engine framework
4+
*
5+
* @copyright Copyright 2019, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*
10+
*/
11+
declare(strict_types=1);
12+
13+
namespace ZEngine\Macro;
14+
15+
use php_user_filter as PhpStreamFilter;
16+
use RuntimeException;
17+
18+
class DefinitionLoader extends PhpStreamFilter
19+
{
20+
/**
21+
* Default PHP filter name for registration
22+
*/
23+
private const FILTER_IDENTIFIER = 'z-engine.def.loader';
24+
25+
/**
26+
* String buffer
27+
*/
28+
private string $data = '';
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function filter($in, $out, &$consumed, $closing)
34+
{
35+
while ($bucket = stream_bucket_make_writeable($in)) {
36+
$this->data .= $bucket->data;
37+
}
38+
39+
if ($closing || feof($this->stream)) {
40+
$consumed = strlen($this->data);
41+
42+
// Simple macros resolving via strtr
43+
$transformedData = strtr($this->data, $this->resolveSystemMacros());
44+
45+
$bucket = stream_bucket_new($this->stream, $transformedData);
46+
stream_bucket_append($out, $bucket);
47+
48+
return PSFS_PASS_ON;
49+
}
50+
51+
return PSFS_FEED_ME;
52+
}
53+
54+
/**
55+
* Wraps given filename with stream resolver
56+
*
57+
* @param string $filename
58+
*/
59+
public static function wrap(string $filename): string
60+
{
61+
// Let's perform self-registration on first query
62+
if (!in_array(self::FILTER_IDENTIFIER, stream_get_filters(), true)) {
63+
self::register();
64+
}
65+
66+
return 'php://filter/read=' . self::FILTER_IDENTIFIER . '/resource=' . $filename;
67+
}
68+
69+
/**
70+
* Register current loader as stream filter in PHP
71+
*
72+
* @throws RuntimeException If registration was failed
73+
*/
74+
private static function register(): void
75+
{
76+
$result = stream_filter_register(self::FILTER_IDENTIFIER, self::class);
77+
if ($result === false) {
78+
throw new RuntimeException('Stream filter was not registered');
79+
}
80+
}
81+
82+
private function resolveSystemMacros(): array
83+
{
84+
$isThreadSafe = ZEND_THREAD_SAFE;
85+
$isWindowsPlatform = stripos(PHP_OS, 'WIN') === 0;
86+
$is64BitPlatform = PHP_INT_SIZE === 8;
87+
88+
// TODO: support ts/nts x86/x64 combination
89+
if ($isThreadSafe || !$is64BitPlatform) {
90+
throw new \RuntimeException('Only x64 non thread-safe versions of PHP are supported');
91+
}
92+
93+
$macros = [
94+
'ZEND_API' => '__declspec(dllimport)',
95+
'ZEND_FASTCALL' => $isWindowsPlatform ? '__vectorcall' : '',
96+
97+
'ZEND_MAX_RESERVED_RESOURCES' => '6',
98+
'ZEND_LIBRARY_NAME' => $isWindowsPlatform ? 'php7.dll' : '',
99+
];
100+
101+
return $macros;
102+
}
103+
}

src/Reflection/FunctionLikeTrait.php

+4-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
namespace ZEngine\Reflection;
1414

15-
use FFI;
1615
use FFI\CData;
1716
use ZEngine\Core;
1817
use ZEngine\Type\OpLine;
@@ -79,18 +78,18 @@ public function redefine(\Closure $newCode): void
7978
$hash = $this->getHash();
8079
if (!isset(self::$originalEntries[$hash])) {
8180
$pointer = Core::new('zend_function *', false);
82-
FFI::memcpy(FFI::addr($pointer), $this->pointer, FFI::sizeof($this->pointer));
81+
Core::memcpy(Core::addr($pointer), $this->pointer, Core::sizeof($this->pointer));
8382
self::$originalEntries[$hash] = $pointer;
8483
}
8584
$selfExecutionState = Core::$executor->getExecutionState();
8685
$newCodeEntry = $selfExecutionState->getArgument(0)->getRawObject();
8786
$newCodeEntry = Core::cast('zend_closure *', $newCodeEntry);
8887

8988
// Copy only common op_array part from original one to keep name, scope, etc
90-
FFI::memcpy($newCodeEntry->func, $this->pointer[0], FFI::sizeof($newCodeEntry->func->common));
89+
Core::memcpy($newCodeEntry->func, $this->pointer[0], Core::sizeof($newCodeEntry->func->common));
9190

9291
// Replace original method with redefined closure
93-
FFI::memcpy($this->pointer, FFI::addr($newCodeEntry->func), FFI::sizeof($newCodeEntry->func));
92+
Core::memcpy($this->pointer, Core::addr($newCodeEntry->func), Core::sizeof($newCodeEntry->func));
9493

9594
}
9695

@@ -128,7 +127,7 @@ public function getOpCodes(): iterable
128127
$totalOpcodes = $this->pointer->op_array->last;
129128
while ($opcodeIndex < $totalOpcodes) {
130129
$opCode = new OpLine(
131-
FFI::addr($this->pointer->op_array->opcodes[$opcodeIndex++])
130+
Core::addr($this->pointer->op_array->opcodes[$opcodeIndex++])
132131
);
133132
yield $opCode;
134133
}

0 commit comments

Comments
 (0)