Buffers

A Zphp\Buffer is a fixed-length block of bytes. It differs from a string in two ways: its bytes can be changed in place, and it moves to another thread instead of being copied.

When a string is passed to a worker, zphp copies it into the worker and copies the result back, so the cost grows with the size of the data. A buffer's bytes change owner without being copied, so passing one costs about the same at any size. Round trips to a worker and back on an Apple M4 Pro, with a release build:

SizeStringBuffer
64 KB0.029 ms0.012 ms
1 MB0.20 ms0.011 ms
16 MB3.9 ms0.012 ms
256 MB73 ms0.016 ms

The buffer times are the cost of submitting and awaiting a task, with no copying. Use a buffer whenever a worker needs large binary data: file contents, images, audio, compressed archives, or network frames.

Moving to another thread

This worker builds an 8 MB binary file, one 64-bit number per entry, and hands it back without a copy:

<?php
$pool = new Zphp\Pool(workers: 1);

$export = $pool->submit(function (int $count) {
    $data = new Zphp\Buffer($count * 8);
    for ($i = 0; $i < $count; $i++) {
        $data->writeInt64LE($i * 8, $i * $i);
    }
    return $data;
}, [1_000_000]);

$data = $export->await();
echo $data->length(), " bytes\n";

$file = fopen('squares.bin', 'wb');
$data->writeTo($file);
fclose($file);
8000000 bytes

Passing a buffer to a worker, returning one from a task, or sending one on a channel moves its bytes. The thread that sent it keeps the Zphp\Buffer object, but it is detached: isDetached() returns true, and any other method throws Zphp\TransferException.

<?php
$data = Zphp\Buffer::fromString('some bytes');
$pool->submit(fn(Zphp\Buffer $data) => $data->length(), [$data])->await();

var_dump($data->isDetached()); // bool(true)
$data->toString();             // throws Zphp\TransferException

Only one thread can use the bytes at a time, so there are no data races and no locks.

Slices

slice($offset, $length) returns a view of part of a buffer without copying. A slice shares its parent's bytes, so a write through either one is visible in both. Omitting the length extends the slice to the end of the buffer.

<?php
$text = Zphp\Buffer::fromString('hello world');
$first = $text->slice(0, 5);
$first->write(0, 'J');
echo $text->toString(), "\n";
Jello world

A slice always refers to the whole block it was cut from. Moving a slice to another thread moves the entire block and detaches the parent and every other slice of it. To send only part of a large buffer, clone the slice first: clone $buffer->slice(0, 1024) copies those 1024 bytes into a new, independent buffer.

Reading and writing

A PNG file stores the image's width and height as 32-bit big-endian numbers at bytes 16 and 20. This reads them without loading the rest of the file:

<?php
$file = fopen('photo.png', 'rb');
$header = new Zphp\Buffer(24);
$header->readFrom($file);
fclose($file);

echo $header->readUInt32BE(16), ' x ', $header->readUInt32BE(20), "\n";
640 x 480

Writing works the same way. This builds a message with a 4-byte length in front of it, a common format for network protocols:

<?php
$message = '{"user":42}';

$packet = new Zphp\Buffer(4 + strlen($message));
$packet->writeUInt32BE(0, strlen($message));
$packet->write(4, $message);

$length = $packet->readUInt32BE(0);
echo $packet->slice(4, $length)->toString(), "\n";
{"user":42}
MethodBehavior
new Zphp\Buffer($length)A buffer of $length zero bytes
Zphp\Buffer::fromString($bytes)A buffer holding a copy of a string
length()The buffer's size in bytes
toString()A string copy of the bytes
write($offset, $data)Copies a string or another buffer in at $offset; overlapping copies are handled
slice($offset, $length = null)A view of part of the buffer
readFrom($stream)One read from a stream straight into the buffer; returns the bytes read, 0 at end of file, or false if the stream cannot be read
writeTo($stream)Writes the buffer to a stream; returns the bytes written, or false if the stream cannot be written
isDetached()Whether the bytes moved to another thread

readFrom and writeTo accept anything fread and fwrite accept, including files, sockets, php://memory, and user stream wrappers. Like fread, one readFrom call can return fewer bytes than the buffer holds, for example from a socket; read into $buffer->slice($read) to continue where the last call stopped.

Fixed-width numbers have a read and a write method for each type:

TypeMethods
8-bitreadInt8, readUInt8, writeInt8, writeUInt8
16-bitreadInt16LE, readInt16BE, readUInt16LE, readUInt16BE, and the matching write methods
32-bitreadInt32LE, readInt32BE, readUInt32LE, readUInt32BE, and the matching write methods
64-bitreadInt64LE, readInt64BE, writeInt64LE, writeInt64BE
FloatsreadFloat32LE, readFloat32BE, readFloat64LE, readFloat64BE, and the matching write methods

Each read takes an offset. Each write takes an offset and a value. A value outside the type's range, or an offset and width that do not fit inside the buffer, throws ValueError. There is no unsigned 64-bit type, because PHP integers are signed.

A buffer never grows. Allocate the size you need, or use slice to work with the part that is filled.

Copies

clone gives an independent copy of a buffer's bytes. serialize() also copies, so a serialized buffer can be stored or sent over the network and unserialized into a new buffer. Only a transfer between threads moves the bytes.