bufferLuauLuwu

A buffer is a fixed-size block of mutable memory. Scripts use buffers to build and parse binary formats like files and network packets, and to store large arrays of numbers in less memory than a table needs.

An external buffer is a buffer whose backing memory is actually owned by the embedder. External buffers can be modified just like buffers owned by the Luwu VM.

A frozen buffer can only be created by embedders and is immutable. Attempting to write to a frozen buffer raises a runtime error; use buffer.isfrozen to check if a buffer is frozen or not.

Most buffer library functions take an offset of bytes from the start of the buffer. Offsets, counts and sizes should be whole non-negative numbers. Offsets starts at byte 0. A read or write outside the buffer throws buffer access out of bounds. Multi-byte values are stored in little endian byte order on every platform.

Summary

Creating and converting

buffer.createLuau
Creates a zero-filled buffer of a given size
buffer.fromstringLuau
Creates a buffer holding a copy of a string's bytes
buffer.tostringLuau
Returns a string holding a copy of a buffer's bytes
buffer.lenLuau
Returns the size of a buffer in bytes

Reading and writing numbers

buffer.read*Luau
Reads the number at an offset as the type the suffix names
buffer.write*LuauLuwu
Writes a number at an offset as the type the suffix names

Strings

buffer.readstringLuau
Returns a string of the bytes starting at an offset
buffer.writestringLuauLuwu
Writes a string's bytes at an offset

Bits

buffer.readbitsLuau
Reads up to 32 bits at a bit offset as an unsigned integer
buffer.writebitsLuauLuwu
Writes the low bits of a value at a bit offset

Integers

buffer.readintegerLuau
Reads 8 bytes at an offset as a signed 64-bit integer
buffer.writeintegerLuauLuwu
Writes an integer as a signed 64-bit value at an offset

Batched reads and writes

buffer.unpack*Luwuflagged
Reads several consecutive values of one type in one call
buffer.pack*Luwuflagged
Writes several values of one type one after another

Copying and comparing

buffer.copyLuauLuwu
Copies bytes from one buffer into another
buffer.fillLuauLuwu
Sets a range of bytes to one value
buffer.memcmpLuwuflagged
Compares the bytes of two buffers

Frozen buffers

buffer.isfrozenLuwuflagged
Checks whether a buffer is a read-only external buffer

Creating and converting

buffer.createLuau

function buffer.create(size: number): buffer

Creates a buffer of size bytes with every byte set to 0. The size limit is 1 GiB (1,073,741,824 bytes) and a negative size is an error.

Input
const packet = buffer.create(4)
buffer.writeu16(packet, 0, 513)

print(buffer.readu8(packet, 0))
print(buffer.readu8(packet, 1))
print(buffer.len(packet))
Output
1
2
4

513 is 0x0201. Little endian order puts its low byte first.

buffer.fromstringLuau

function buffer.fromstring(str: string): buffer

Creates a buffer that holds a copy of the bytes of str. Its size is the string's length in bytes.

buffer.tostringLuau

function buffer.tostring(b: buffer): string

Returns a string holding a copy of every byte in the buffer.

buffer.lenLuau

function buffer.len(b: buffer): number

Returns the size of the buffer in bytes.

Reading and writing numbers

buffer.read*Luau

function buffer.readi8(b: buffer, offset: number): number
function buffer.readu8(b: buffer, offset: number): number
function buffer.readi16(b: buffer, offset: number): number
function buffer.readu16(b: buffer, offset: number): number
function buffer.readi32(b: buffer, offset: number): number
function buffer.readu32(b: buffer, offset: number): number
function buffer.readf32(b: buffer, offset: number): number
function buffer.readf64(b: buffer, offset: number): number

Reads the bytes at offset as the type the suffix names and returns them as a number. Integers use two's complement and floating-point numbers use IEEE 754:

SuffixTypeRange
i8signed 8-bit integer-128 to 127
u8unsigned 8-bit integer0 to 255
i16signed 16-bit integer-32,768 to 32,767
u16unsigned 16-bit integer0 to 65,535
i32signed 32-bit integer-2,147,483,648 to 2,147,483,647
u32unsigned 32-bit integer0 to 4,294,967,295
f3232-bit floating-point numbersingle-precision IEEE 754
f6464-bit floating-point numberdouble-precision IEEE 754

A floating-point value whose bits are a NaN may come back as a different quiet NaN.

buffer.write*LuauLuwu

function buffer.writei8(b: buffer, offset: number, value: number)
function buffer.writeu8(b: buffer, offset: number, value: number)
function buffer.writei16(b: buffer, offset: number, value: number)
function buffer.writeu16(b: buffer, offset: number, value: number)
function buffer.writei32(b: buffer, offset: number, value: number)
function buffer.writeu32(b: buffer, offset: number, value: number)
function buffer.writef32(b: buffer, offset: number, value: number)
function buffer.writef64(b: buffer, offset: number, value: number)

Converts value to the type the suffix names and writes its bytes at offset. The ranges are in the table under buffer.read*.

Integer writes convert the number by the same rules as the bit32 library. A value out of range keeps only its low bits: writing 43,981 (0xabcd) with writei8 stores 0xcd and reads back as -51. Converting infinity or NaN to an integer gives a platform-specific result.

Input
const bytes = buffer.create(1)
buffer.writei8(bytes, 0, 0xabcd)

print(buffer.readi8(bytes, 0))
print(buffer.readu8(bytes, 0))
Output
-51
205

Strings

buffer.readstringLuau

function buffer.readstring(b: buffer, offset: number, count: number): string

Returns a string of the count bytes starting at offset.

buffer.writestringLuauLuwu

function buffer.writestring(b: buffer, offset: number, value: string, count: number?)

Writes the bytes of value at offset. When count is given, only the first count bytes are written. A count larger than the string's length throws string length overflow.

Input
const header = buffer.create(8)
buffer.writestring(header, 0, "LUWU")
buffer.writestring(header, 4, "v1.0", 2)

print(buffer.readstring(header, 0, 6))
Output
LUWUv1

Bits

buffer.readbitsLuau

function buffer.readbits(b: buffer, bit_offset: number, bit_count: number): number

Reads bit_count bits starting at bit bit_offset and returns them as an unsigned integer. Bit 0 is the lowest bit of the first byte. bit_count must be between 0 and 32.

buffer.writebitsLuauLuwu

function buffer.writebits(b: buffer, bit_offset: number, bit_count: number, value: number)

Writes the low bit_count bits of value starting at bit bit_offset. The bits around them don't change. bit_count must be between 0 and 32.

Input
const flags = buffer.create(1)
buffer.writebits(flags, 2, 3, 0b101)

print(buffer.readu8(flags, 0))
print(buffer.readbits(flags, 2, 3))
Output
20
5

Integers

buffer.readintegerLuau

function buffer.readinteger(b: buffer, offset: number): integer

Reads the 8 bytes at offset as a signed 64-bit integer and returns it as an integer.

The type checker only declares it when LuauIntegerType2 is on as well.

buffer.writeintegerLuauLuwu

function buffer.writeinteger(b: buffer, offset: number, value: integer)

Writes value as a signed 64-bit integer to the 8 bytes at offset. It takes an integer and a number is an error.

const ids = buffer.create(8)
buffer.writeinteger(ids, 0, integer.maxsigned)

print(buffer.readinteger(ids, 0) == integer.maxsigned) --> true

The type checker only declares it when LuauIntegerType2 is on as well.

Batched reads and writes

buffer.unpack*Luwuflagged

function buffer.unpacki8(b: buffer, offset: number, count: number): ...number
function buffer.unpacku8(b: buffer, offset: number, count: number): ...number
function buffer.unpacki16(b: buffer, offset: number, count: number): ...number
function buffer.unpacku16(b: buffer, offset: number, count: number): ...number
function buffer.unpacki32(b: buffer, offset: number, count: number): ...number
function buffer.unpacku32(b: buffer, offset: number, count: number): ...number
function buffer.unpackf32(b: buffer, offset: number, count: number): ...number
function buffer.unpackf64(b: buffer, offset: number, count: number): ...number

Reads count consecutive values of the type the suffix names, starting at offset, and returns all of them. It is one call where buffer.read* would need one call per value. count must be at least 1.

The whole batch is bounds checked before the first read. A batch that runs past the end of the buffer throws and returns nothing. A batch larger than the VM's stack allows throws too many values to unpack.

When count is a whole number literal from 1 to 4096, the type checker gives the call exactly that many number results. The values can then go straight into a function with a fixed parameter list:

Input
const points = buffer.create(24)
buffer.packf32(points, 0, 1, 2, 3, 4, 5, 6)

const first = vector.create(buffer.unpackf32(points, 0, 3))
const second = vector.create(buffer.unpackf32(points, 12, 3))
print(`{first}`)
print(`{second}`)
Output
1, 2, 3
4, 5, 6

buffer.pack*Luwuflagged

function buffer.packi8(b: buffer, offset: number, ...: number)
function buffer.packu8(b: buffer, offset: number, ...: number)
function buffer.packi16(b: buffer, offset: number, ...: number)
function buffer.packu16(b: buffer, offset: number, ...: number)
function buffer.packi32(b: buffer, offset: number, ...: number)
function buffer.packu32(b: buffer, offset: number, ...: number)
function buffer.packf32(b: buffer, offset: number, ...: number)
function buffer.packf64(b: buffer, offset: number, ...: number)

Writes every value after offset as the type the suffix names, one after another. The number of values sets the batch size, and passing none writes nothing. Each value is converted the way buffer.write* converts it.

Nothing is written unless the whole batch fits in the buffer and every value is a number. A failed call never leaves a half-written batch behind.

Combined with an unpack function, it converts values from one width to another:

Input
const wide = buffer.create(12)
buffer.packu32(wide, 0, 10, 300, 65)

const narrow = buffer.create(3)
buffer.packu8(narrow, 0, buffer.unpacku32(wide, 0, 3))

print(buffer.readu8(narrow, 0))
print(buffer.readu8(narrow, 1))
print(buffer.readu8(narrow, 2))
Output
10
44
65

300 doesn't fit in a byte. packu8 writes its low 8 bits, 44.

Copying and comparing

buffer.copyLuauLuwu

function buffer.copy(
    target: buffer,
    target_offset: number,
    source: buffer,
    source_offset: number?,
    count: number?
)

Copies count bytes from source at source_offset into target at target_offset. source_offset defaults to 0. When count is omitted, everything from source_offset to the end of source is copied.

source and target can be the same buffer. An overlapping copy behaves as if the source bytes went through a temporary buffer first:

Input
const text = buffer.fromstring("abcdef")
buffer.copy(text, 2, text, 0, 4)

print(buffer.tostring(text))
Output
ababcd

buffer.fillLuauLuwu

function buffer.fill(b: buffer, offset: number, value: number, count: number?)

Sets count bytes starting at offset to value. Only the low 8 bits of value are used. When count is omitted, every byte from offset to the end of the buffer is set.

buffer.memcmpLuwuflagged

function buffer.memcmp(
    b1: buffer,
    b2: buffer,
    offset1: number?,
    offset2: number?,
    count: number?
): number

Compares count bytes of b1 starting at offset1 with the bytes of b2 starting at offset2. Both offsets default to 0. When count is omitted, it is the smaller of the two remaining lengths.

Bytes compare as unsigned values in order, and the result is always -1, 0 or 1:

It compares without a loop of reads in the script and without turning the buffers into strings first. The ordering also makes it usable as a sort key.

Input
const a = buffer.fromstring("abc")
const b = buffer.fromstring("abd")
const tagged = buffer.fromstring("id:abc")

print(buffer.memcmp(a, b))
print(buffer.memcmp(b, a))
print(buffer.memcmp(a, tagged, 0, 3))
Output
-1
1
0

Frozen buffers

buffer.isfrozenLuwuflagged

function buffer.isfrozen(b: buffer): boolean

Returns true when the buffer is frozen. A frozen buffer is a read-only external buffer that the host created. Every function that writes to it throws buffer is immutable.

Buffers made by buffer.create and buffer.fromstring are never frozen:

Input
const bytes = buffer.create(8)
print(buffer.isfrozen(bytes))
Output
false