rayblair06 / php-struct
A lightweight PHP class that lets you define C-style structs using PHP FFI, enabling memory-efficient, typed data structures for high-performance workloads.
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/rayblair06/php-struct
Requires
- ext-ffi: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.70
- phpunit/phpunit: ^9.0
Suggests
- ext-ffi: Required for native struct support
README
A lightweight PHP class that lets you define C-style structs using PHP FFI, enabling memory-efficient, typed data structures for high-performance workloads.
Instead of juggling large PHP arrays with unpredictable memory usage, Struct lets you allocate a fixed-size, type-safe memory block for predictable performance and compact memory layout — just like in C.
💡 Inspiration
This project is inspired by the simplicity of C data structures, adapted for PHP developers who need predictable memory layouts and FFI-level performance without writing C extensions.
🧠 Example Use Cases
- Simulation / physics data storage
- Game development prototypes
- Data streaming and compression
- Machine learning preprocessing in PHP
- Memory-efficient caches or lookup tables
Summary
- 💡 Inspiration
- 🧠 Example Use Cases
- 🚀 Features
- 🧰 Requirements
- 🧪 Installation
- 🧱 Example Usage
- 🔍 API Reference
- ⚠️ Caveats
- ⚙️ Performance Benchmark
- 📄 License
🚀 Features
- Define custom C structs at runtime using
typedef - Store thousands of records in fixed-size, contiguous memory
- Typed memory fields (
int,float, etc.) - Implements
IteratorandCountable - Provides
sizeof()and total memory usage - Ideal for numerical data, simulation, or high-frequency workloads
🧰 Requirements
- PHP 8.0+
- FFI extension enabled (
--with-ffi) - CLI SAPI (FFI is not enabled by default in web contexts)
🧪 Installation
Install via Composer once packaged:
composer require rayblair06/php-struct
🧱 Example Usage
<?php $count = 100_000; $typedef = " typedef struct { int id; float value; } Record; "; $records = new Struct($typedef, "Record", $count); // Populate with data for ($i = 0; $i < $count; $i++) { $records->set($i, [ 'id' => $i, 'value' => (float)$i * 1.5 ]); } // Retrieve a record at specific index print_r($records->get(0)); echo 'Struct size: ' . $records->sizeof() . " bytes\n"; echo 'Total memory: ' . $records->totalBytes() . " bytes\n";
🔍 API Reference
__construct(string $typedef, string $typeName, int $count)
Create a new struct array of a given type and size.
$records = new Struct($typedef, "Record", 10_000);
set(int $index, array $data): void
Set field values for a struct at the given index.
$records->set(0, ['id' => 1, 'value' => 42.0]);
get(int $index): array
Retrieve all field values as an associative array.
$record = $records->get(0);
count(): int
Get total number of structs allocated.
echo count($records);
sizeof(): int
Get the size in bytes of a single struct instance.
totalBytes(): int
Get total allocated memory size (count × sizeof).
getRawBuffer(): FFI\CData
Access the underlying raw C buffer (advanced usage).
⚠️ Caveats
General
- Only primitive C types (
int,float,char, etc.) are supported. - The FFI extension must be explicitly enabled in your PHP environment and is only meant for CLI, daemon, or worker processes.
- Structs are stored in native memory — do not exceed memory limits.
- Works best for read-heavy, data-structured workloads (not objects with complex behavior).
Memory Management
By default, FFI::new() allocates memory inside PHP’s managed heap, not raw C memory.
That means the memory used by your struct arrays is automatically freed when the Struct object is destroyed or goes out of scope.
There’s no need to manually call FFI::free() or perform explicit cleanup.
However:
- In long-running scripts (e.g., daemons or workers), memory will only be reclaimed once objects are destroyed.
- To release memory sooner, you can call:
unset($records); gc_collect_cycles();
For most PHP workloads — including large data processing or game loops — you can safely rely on PHP’s automatic memory management.
Not Thread-Safe (in parallel FFI contexts)
If you use multithreading (e.g., pthreads, parallel, or ReactPHP workers):
- FFI instances are not thread-safe.
- Each thread/process must create its own FFI context.
This should be fine in CLI single-threaded s.
Structs Must Use Fixed Layouts
FFI does not support:
- Flexible array members (
struct { int n; float values[]; }) - Bitfields
- Complex unions or function pointers
Keep your typedefs simple: plain scalar fields or nested structs.
Alignment and Padding
C compilers naturally pad structs to align fields on word boundaries. PHP’s FFI follows your platform’s ABI rules, so:
typedef struct { char a; int b; } Example;
may take 8 bytes instead of 5 (due to padding).
Always use $struct->sizeof() to get the true per-item size.
Don’t assume it’s the sum of field sizes.
Strings and Character Arrays
Storing text requires care:
typedef struct { char name[32]; } Record;
You can assign strings to char[] via:
FFI::memcpy($struct->name, "Hello", 6);
But reading them back requires manual null-termination checks. It’s better to avoid strings inside structs unless you know what you’re doing.
Cross-Platform Differences
- On Windows,
long= 32 bits - On Linux/macOS,
long= 64 bits
So avoid platform-dependent types like long — use int32_t, int64_t, etc., or int, float, double for consistency.
Type Validation and Safety
- FFI doesn’t type-check at runtime the way PHP does.
- Assigning an invalid value (e.g., a string to a float field) will be silently truncated or converted.
For production-critical code, consider adding lightweight PHP-side validation before writing to the struct buffer.
Serialization & Persistence
Struct data lives in native memory — it’s not serializable via PHP’s normal mechanisms (serialize(), json_encode(), etc.) without converting to arrays.
You can, however, export easily:
$data = array_map(fn($r) => $r, iterator_to_array($records));
Floating-Point Precision Notice
Because this library uses native C data types via PHP FFI, floating-point values are stored as 32-bit floats, not PHP’s default 64-bit double.
This can lead to small rounding differences when reading values back into PHP — for example:
$record->value = 3.14; // stored as C float var_dump($record->value); // float(3.140000104904175)
This is expected behavior for IEEE 754 32-bit floats and not a bug.
If you need to compare floats in tests or application logic, use approximate comparisons instead of strict equality:
$this->assertEqualsWithDelta(3.14, $record['value'], 0.000001);
Or, if you require full double-precision, you can modify your typedefs to use double instead of float:
typedef struct { int id; double value; } Record;
⚙️ Performance Benchmark
To compare Struct memory usage with standard PHP data structures, run the included benchmark script:
php -d memory_limit=512M bin/benchmarks.php
🧠 What It Does
The script allocates 500,000 records using four different approaches:
| Type | Description | Expected Memory Footprint |
|---|---|---|
| Struct | Fixed-size native memory buffer using FFI | 🔹 Lowest (~a few MB) |
| Array | Standard PHP associative arrays | ⚠️ High (~100–300 MB) |
| stdClass | Dynamically assigned object properties | ⚠️ Very High |
| RecordClass | User-defined class instances | ⚠️ Extremely High |
📈 Interpretation
- The Struct implementation consumes ~30–70× less memory than PHP arrays or objects for the same data.
- Performance remains comparable for reads and writes.
- This efficiency scales linearly with the number of records, making it ideal for large datasets or real-time numerical applications.
📄 License
MIT © 2025 — Crafted for performance and clarity.