Skip to content

FFI Internals

Understanding how NDArray uses PHP's Foreign Function Interface (FFI) to communicate with Rust will help you write more efficient code.

Architecture Overview

┌─────────────────────────────────────────┐
│           PHP Code                      │
│  $array->add(5)->multiply(2)           │
└──────────────┬──────────────────────────┘
               │ FFI Call

┌─────────────────────────────────────────┐
│        Rust ndarray Library             │
│  - Memory allocation                    │
│  - Computation (SIMD, parallel)         │
│  - Data storage                         │
└─────────────────────────────────────────┘

What is FFI?

PHP's FFI (Foreign Function Interface) allows PHP code to call functions written in C (or languages that can produce C-compatible libraries, like Rust). NDArray uses FFI to:

  1. Allocate memory in Rust for array data
  2. Perform computations using optimized Rust code
  3. Transfer data between PHP and Rust

Memory Management

Rust Owns the Data

All NDArray data lives in Rust-allocated memory:

php
$arr = NDArray::zeros([1000, 1000]);
// Memory allocated in Rust, PHP holds only a pointer

PHP Holds Opaque Pointers

PHP NDArray objects contain:

  • An opaque pointer to Rust memory ($handle)
  • View metadata (shape, strides, offset) managed in PHP
  • Reference to parent array (for views)

Automatic Cleanup

When an NDArray is destroyed:

php
{
    $arr = NDArray::random([1000, 1000]);
    // ... use $arr ...
} // Destructor called → Rust memory freed (only for root arrays)

Root arrays (where $base is null) free Rust memory on destruction. Views keep their root alive through PHP's reference counting.

View Memory Model

php
$root = NDArray::random([1000, 1000]);  // Root array
$view = $root->slice(['0:100']);         // View shares handle

// $view->handle === $root->handle (same Rust memory)
// $view has different shape/strides/offset
// $view->base points to $root

Views do not free memory - only the root array does.

FFI Call Patterns

Each Method = One (or more) FFI Calls

php
// Each operation makes FFI call(s) to Rust
$a = $data->add(5);        // 1 FFI call
$b = $a->multiply(2);      // 1 FFI call  
$c = $b->sqrt();           // 1 FFI call

// Chaining is the same as separate calls
$result = $data->add(5)->multiply(2)->sqrt();
// 3 FFI calls total

FFI Call Overhead

  • Small arrays (< 100 elements): FFI overhead is significant
  • Large arrays (> 1000 elements): FFI overhead is negligible
  • Always prefer vectorized ops over PHP loops, even with FFI overhead

Data Type Handling

Type Conversion

When data moves between PHP and Rust:

php
// PHP → Rust: Conversion happens during array creation
$arr = NDArray::array([1, 2, 3], DType::Float32);
// PHP integers converted to Rust f32

// Rust → PHP: Conversion during scalar access
$value = $arr[0];
// Rust f32 converted to PHP float

Supported Types

NDArray supports all FFI-compatible types:

DTypeRust TypePHP TypeSize
Int8i8integer1 byte
Int16i16integer2 bytes
Int32i32integer4 bytes
Int64i64integer8 bytes
UInt8u8integer1 byte
UInt16u16integer2 bytes
UInt32u32integer4 bytes
UInt64u64integer8 bytes
Float32f32float4 bytes
Float64f64float8 bytes
Boolu8boolean1 byte

Error Handling

Rust Panics → PHP Exceptions

Rust code never panics to PHP. All errors are converted to exceptions:

php
try {
    $result = $a->divide($b);  // Division by zero in Rust
} catch (NDArrayException $e) {
    // Caught as PHP exception
    echo $e->getMessage();
}

Error Types

  • NDArrayException: General errors (invalid operations, allocation failures)
  • ShapeException: Shape mismatch errors
  • IndexException: Invalid indexing

Zero-Copy Operations

Some operations require no FFI calls at all:

View Creation (Pure PHP)

php
$view = $arr->slice(['0:10']);  // No FFI call!
// Shape/strides/offset calculated in PHP
// Same Rust handle shared

Partial Indexing (Pure PHP)

php
$row = $matrix[0];  // No FFI call!
// Returns view with updated metadata

Best Practices

1. Minimize PHP Loops

php
// Bad: Thousands of FFI calls
for ($i = 0; $i < 1000; $i++) {
    $sum += $arr[$i];  // FFI call per access
}

// Good: One FFI call
$sum = $arr->sum();  // Single FFI call, Rust handles iteration

2. Use Vectorized Operations

php
// Bad: Multiple small operations
for ($i = 0; $i < $n; $i++) {
    $result[$i] = sin($arr[$i]) * 2;
}

// Good: Single vectorized operation
$result = $arr->sin()->multiply(2);

3. Batch Element Access

php
// Bad: Individual element access
$values = [];
for ($i = 0; $i < 100; $i++) {
    $values[] = $arr[$i];
}

// Good: Convert to PHP array once
$values = $arr->slice(['0:100'])->toArray();

Troubleshooting FFI Issues

Memory Leaks

If memory grows unexpectedly:

php
// Check for circular references
$arr = NDArray::random([1000, 1000]);
$view = $arr->slice(['0:10']);
// Both reference each other - but PHP GC handles this

// Explicit cleanup if needed
unset($view);
unset($arr);

Performance Debugging

php
// Profile FFI calls
$start = microtime(true);
$result = $arr->sum();
$ffiTime = microtime(true) - $start;
echo "FFI call took: {$ffiTime}s\n";

Type Issues

php
// Check dtype if operations fail
$arr = NDArray::array([1, 2, 3], DType::Int32);
echo $arr->dtype()->value;  // "int32"

// Some ops require specific types
$arr->sqrt();  // Works on all types, returns appropriate type

See Also

Released under the MIT License.