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

0.1.0 2025-11-10 08:00 UTC

This package is auto-updated.

Last update: 2025-11-10 08:05:16 UTC


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

🚀 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 Iterator and Countable
  • 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.