klitsche/ffigen
CLI tool to generate and update low-level PHP FFI bindings from C headers. Produces constants.php and Methods.php (static method wrappers with phpdoc). Configurable via .ffigen.yml and optional custom parser hooks for preprocessing.
Install the Package
Add to composer.json under require-dev:
composer require --dev klitsche/ffigen
Create a Configuration File
Generate a .ffigen.yml in your project root with basic settings:
headerFiles:
- /path/to/library.h
libraryFile: /path/to/liblibrary.so
outputPath: ./generated/
namespace: App\FFI\Bindings
Run the Generator Execute the CLI command:
vendor/bin/ffigen
This generates:
constants.php (autoloadable constants)Methods.php (static method bindings with PHPDoc)Autoload Generated Files
Add constants.php to composer.json under "autoload":
"autoload": {
"files": ["generated/constants.php"]
}
Run:
composer dump-autoload
Use in Laravel Load the bindings in a Service Provider:
use App\FFI\Bindings\Methods;
class FFIServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('ffi.bindings', function () {
return new class {
public function callNativeMethod()
{
return Methods::nativeFunction(...);
}
});
});
}
}
Example: Integrate libuuid for generating UUIDs.
libuuid-dev on your system..ffigen.yml:
headerFiles:
- /usr/include/uuid/uuid.h
libraryFile: libuuid.so
outputPath: ./app/FFI/UUID/
namespace: App\FFI\UUID
excludeMethods:
- /^uuid_.*_free$/
vendor/bin/ffigen
use App\FFI\UUID\Methods;
$uuid = Methods::uuid_generate();
Generate Bindings on Build
Add a script to composer.json:
"scripts": {
"post-install-cmd": [
"@ffigen"
],
"ffigen": "vendor/bin/ffigen"
}
Bindings are auto-generated on composer install.
Version-Lock Bindings
Commit constants.php and Methods.php to Git (or use a vendor/ subdirectory). Regenerate only when C headers change.
Dynamic Updates (Advanced) Use a custom command to regenerate bindings on demand:
// app/Console/Commands/RegenerateFFIBindings.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class RegenerateFFIBindings extends Command
{
protected $signature = 'ffi:regenerate';
protected $description = 'Regenerate FFI bindings';
public function handle()
{
$process = new Process(['vendor/bin/ffigen']);
$process->run();
$this->info($process->getOutput());
}
}
Run with:
php artisan ffi:regenerate
Facade for Clean API Create a facade to abstract FFI calls:
// app/Facades/FFI.php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class FFI extends Facade
{
protected static function getFacadeAccessor()
{
return 'ffi.bindings';
}
}
Usage:
use App\Facades\FFI;
$result = FFI::uuidGenerate();
Error Handling Wrapper Wrap FFI calls in a service class to handle exceptions:
// app/Services/FFIService.php
namespace App\Services;
use App\FFI\UUID\Methods;
use FFI\Exception;
class FFIService
{
public function generateUUID()
{
try {
return Methods::uuid_generate();
} catch (Exception $e) {
\Log::error("FFI Error: " . $e->getMessage());
throw new \RuntimeException("Failed to generate UUID", 0, $e);
}
}
}
Type Mapping for Laravel Eloquent Convert C structs to Laravel collections/models:
use App\FFI\SomeLibrary\Methods;
$struct = Methods::someFunction();
$data = [
'field1' => $struct->field1,
'field2' => $struct->field2,
];
return new SomeModel($data);
Header File Paths
.ffigen.yml to avoid issues across environments./usr/include/ or /usr/local/include/.Namespace Strategy
App\FFI\Library).Exclusion Patterns
excludeMethods/excludeConstants to filter unwanted bindings:
excludeMethods:
- /^internal_/
- /_test$/
Custom Parser for Complex Types
Extend the default parser to handle edge cases (e.g., void* pointers, custom structs):
// app/FFI/CustomParser.php
namespace App\FFI;
use Klitsche\FFIGen\Config;
use Klitsche\FFIGen\Adapter\PHPCParser\Parser;
class CustomParser extends Parser
{
public function __construct(Config $config)
{
parent::__construct($config);
// Add custom type mappings
$this->context->define('MY_CUSTOM_TYPE', 'int');
}
protected function parseHeaderFile(string $file): array
{
// Preprocess headers (e.g., add includes)
$preprocessed = $this->preprocessHeaders($file);
return parent::parseHeaderFile($preprocessed);
}
}
Update .ffigen.yml:
parserClass: App\FFI\CustomParser
Testing Bindings Write PHPUnit tests to verify generated bindings:
// tests/FFI/UUIDTest.php
namespace Tests\FFI;
use App\FFI\UUID\Methods;
use PHPUnit\Framework\TestCase;
class UUIDTest extends TestCase
{
public function testGenerateUUID()
{
$uuid = Methods::uuid_generate();
$this->assertRegExp('/^[0-9a-f]{32}$/', $uuid);
}
}
FFI Extension Missing
Class 'FFI' not found.php.ini or use a Docker image with FFI preinstalled (e.g., php:8.2-fpm with docker-php-ext-install ffi).Library Not Found
Failed to load library: liblibrary.so.ldconfig -p to list available libraries on Linux).LD_LIBRARY_PATH before running PHP:
export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH
vendor/bin/ffigen
Header File Not Found
File not found: library.h..ffigen.yml.$this->context->headerSearchPaths = ['/usr/include', '/opt/local/include'];
Type Mismatches
Type error: Expected int, got string.void* to resource).FFI::type() to define custom types:
$ffi = FFI::cdef("typedef struct { int id; } MyStruct;");
Memory Leaks
malloc in C)._free for allocations (e.g., uuid_unparse returns a buffer that must be freed).$buffer = Methods::uuid_unparse($uuid);
$result = Methods
How can I help you explore Laravel packages today?