buffer: Growable byte storage

Buffer Describes initialization, addition, inquiry, capacity and release rules.

Wave Foundation

Meaning of Buffer

Buffer in std::buffer::types has data: ptr<u8>, len: i64, and cap: i64. len is the number of bytes initialized and in use, and cap is the total number of bytes allocated. Always maintain 0 <= len <= cap. The string NUL does not automatically guarantee termination.

Basic API

module declaration meaning
std::buffer::alloc buffer_init(out_buf: ptr<Buffer>, capacity: i64) -> i64 Initialize new repository. Do not recall into buffers already owned
same module buffer_free(buf: ptr<Buffer>) -> i64 Deallocate. Empty if successful
same module buffer_reserve(buf: ptr<Buffer>, required_cap: i64) -> i64 Ensure minimum full capacity. len Maintained
same module buffer_clear(buf: ptr<Buffer>) -> i64 Maintain capacity and len=0
same module buffer_resize(buf: ptr<Buffer>, new_len: i64, value: u8) -> i64 Change length, fill new bytes with value
std::buffer::write buffer_push(buf: ptr<Buffer>, value: u8) -> i64 add one byte
same module buffer_append(buf: ptr<Buffer>, src: ptr<u8>, size: i64) -> i64 sizeCopy and add byte
same module buffer_append_str(buf: ptr<Buffer>, s: str) -> i64 Add string bytes excluding NUL
std::buffer::read buffer_get(buf: ptr<Buffer>, index: i64, out_value: ptr<u8>) -> i64 Read one byte in range

Status return API returns BUFFER_OK(0) on success. Distinguish between errors INVALID, BOUNDS, OVERFLOW, and ALLOC from std::buffer::error. The number is not interpreted as OS errno. buffer_new represents an allocation failure as an empty Buffer, so when you need to distinguish between failures, use buffer_init.

Running example

import("std::buffer::alloc")::{
    Buffer, buffer_init, buffer_free
};
import("std::buffer::write")::{
    buffer_append_str, buffer_push
};
import("std::buffer::read")::{
    buffer_get
};

fun main() -> i32 {
    var data: Buffer;
    if (buffer_init(&data, 0) < 0) {
        return 1;
    }
    if (buffer_append_str(&data, "Hi") < 0 || buffer_push(&data, 33) < 0) {
        buffer_free(&data);
        return 2;
    }

    var value: u8 = 0;
    if (buffer_get(&data, 2, &value) < 0) {
        buffer_free(&data);
        return 3;
    }

    println("{} {}", data.len, value);
    if (buffer_free(&data) < 0) {
        return 4;
    }

    return 0;
}

Execution result:

3 33

Save it as main.wave and run it as wavec run main.wave. An initial capacity of 0 is not a failure, but a valid empty buffer. Frees up space during further processing.

Lifespan and Failure

Growing the buffer can change data. Do not use a previously borrowed address after an operation that may reallocate. Copying the Buffer structure does not duplicate its allocation, so give that allocation a single owner.

buffer_get does not change the output arguments if it fails. On the other hand, the convenience function buffer_at also represents errors as 0, so use buffer_get to distinguish between actual 0 bytes and failures. Avoid creating invalid len/cap by directly changing public fields.

Memory API · Practice reading a file as Buffer

Observe length and capacity separately

reserve frees up storage space, but does not increase len. resize changes the actual length used and initializes the extended portion to the specified byte. clear sets only the length used to 0, allowing allocation to be reused.

Save the following program as main.wave and run it. It doesn't rely on the exact growth multiple of capacity; it just ensures that you have the space you need.

import("std::buffer::alloc")::{
    Buffer,
    buffer_init,
    buffer_reserve,
    buffer_resize,
    buffer_clear,
    buffer_free
};

fun main() -> i32 {
    var data: Buffer;

    if (buffer_init(&data, 0) < 0) {
        return 1;
    }

    if (buffer_reserve(&data, 20) < 0) {
        buffer_free(&data);
        return 2;
    }

    println("reserved length={}", data.len);

    if (buffer_resize(&data, 3, 7) < 0) {
        buffer_free(&data);
        return 3;
    }

    println("resized length={} first={}", data.len, deref data.data[0]);

    if (buffer_clear(&data) < 0) {
        buffer_free(&data);
        return 4;
    }

    println("cleared length={}", data.len);

    if (buffer_free(&data) < 0) {
        return 5;
    }

    return 0;
}

Execution result:

reserved length=0
resized length=3 first=7
cleared length=0

When increasing to resize, we passed value=7, so all three new bytes we see are 7. Space only secured with reserve is not read as initialized data. cap will remain after clear and can be added again to the same Buffer.

Distinguish between zero bytes and lookup failures

buffer_get returns the status and writes the actual bytes as output arguments. Even if the data is 0, it is a normal success.

import("std::buffer::alloc")::{Buffer, buffer_init, buffer_free};
import("std::buffer::write")::{buffer_push};
import("std::buffer::read")::{buffer_get};
import("std::buffer::error")::{BUFFER_ERR_BOUNDS};

fun main() -> i32 {
    var data: Buffer;

    if (buffer_init(&data, 0) < 0) {
        return 1;
    }

    if (buffer_push(&data, 0) < 0) {
        buffer_free(&data);
        return 2;
    }

    var value: u8 = 99;

    if (buffer_get(&data, 0, &value) < 0) {
        buffer_free(&data);
        return 3;
    }

    println("stored={}", value);
    value = 99;

    if (buffer_get(&data, 1, &value) == BUFFER_ERR_BOUNDS) {
        println("outside, preserved={}", value);
    }

    if (buffer_free(&data) < 0) {
        return 4;
    }

    return 0;
}

Execution result:

stored=0
outside, preserved=99

The first hit is a success reading 0, the second hit is an out-of-bounds failure. Even if value=99 remains after a failure, it does not mean that it is the value read from the buffer. Be sure to check the status together.

Practice solution: Byte accumulation

To add numbers 0 through 9, repeat buffer_push and check each result. Store the sum in i64 and read only the range 0 <= index < data.len. After handling the buffer, we call buffer_free on both the success and failure paths.

import("std::buffer::alloc")::{Buffer, buffer_init, buffer_free};
import("std::buffer::write")::{buffer_push};

fun main() -> i32 {
    var data: Buffer;

    if (buffer_init(&data, 0) < 0) {
        return 1;
    }

    for (var value: i32 = 0; value < 10; value += 1) {
        if (buffer_push(&data, value as u8) < 0) {
            buffer_free(&data);
            return 2;
        }
    }

    var total: i64 = 0;

    for (var index: i64 = 0; index < data.len; index += 1) {
        total += deref data.data[index] as i64;
    }

    println("sum={}", total);

    if (buffer_free(&data) < 0) {
        return 3;
    }

    return 0;
}

Execution result:

sum=45