fs and io: Files and byte transfers

Describes file lifetime, full read, partial transfer and post-failure states.

Wave Foundation

Select file API

The convenience function of std::fs::file receives the path and performs the necessary opening and closing. Functions that return a descriptor must be closed by the caller.

declaration Success results and precautions
open_read(path: str) -> i64 Open descriptor. Negative numbers are errors
create(path: str) -> i64 Create or delete existing file contents. Returns the owning descriptor
open_append(path: str) -> i64 Open or create for addition
size(path: str) -> i64 Number of bytes. Negative numbers are errors
read_into(path: str, dst: ptr<u8>, dst_cap: i64) -> i64 Number of bytes in the entire file. Lack of capacity is an error
read_to_end(path: str, dst_buffer: ptr<Buffer>) -> i64 Add file after existing Buffer and return additional amount
write(path: str, src: ptr<u8>, len: i64) -> i64 Number of bytes written replacing existing content
append(path: str, src: ptr<u8>, len: i64) -> i64 Number of bytes added to the end
remove(path: str) -> i64 removal status. failure is negative

false of exists(path) alone cannot distinguish between missing files and permission errors. Be sure to check the actual open result, as the state may change between checking for existence and opening.

Low level I/O

std::io::fd
io_read(fd: i64, buf: ptr<u8>, len: i64) -> i64
io_write(fd: i64, buf: ptr<u8>, len: i64) -> i64
io_read_exact(fd: i64, buf: ptr<u8>, len: i64) -> i64
io_write_all(fd: i64, buf: ptr<u8>, len: i64) -> i64
io_close(fd: i64) -> i64

The positive result of io_read is the number of bytes read, and 0 in a positive length request is EOF. io_write may be written less than requested. If a full transfer is required, use the exact/all function. Still, we do not assume that the failure reverts the external state, as some transfers may have occurred before the error.

io_read_exact is an error if EOF is encountered before the required length. read_into returns IO_ERR_NO_SPACE if the buffer is full, and some bytes may have already been written. The read function does not automatically append NUL to the end of the string.

Buffer and error handling

On failure, read_to_end restores the original len, but its capacity and data address may have changed. The caller must free the Buffer after either success or failure. File-writing APIs do not guarantee atomic file replacement.

From File reading practice, you can run the program from import to release. Consider the path/permission differences in Linux/macOS/Windows/FreeBSD and the accessible directory restrictions in WASI. It does not directly interpret the descriptor value as a raw handle of another OS.

Reading large files into small buffers

Operations that do not require the entire file to be placed in memory can be handled with fixed buffers and read iterations. The following program prints the contents of input.txt and counts the total number of bytes read. Save one Wave and LF in the input file.

import("std::fs::file")::{open_read};
import("std::io::fd")::{io_read, io_write_all, io_close};
import("std::io::consts")::{IO_STDOUT_FD};

fun main() -> i32 {
    var descriptor: i64 = open_read("input.txt");

    if (descriptor < 0) {
        return 1;
    }

    var buffer: array<u8, 4>;
    var total: i64 = 0;

    while (true) {
        var count: i64 = io_read(descriptor, &buffer[0], 4);

        if (count < 0) {
            io_close(descriptor);
            return 2;
        }

        if (count == 0) {
            break;
        }

        if (io_write_all(IO_STDOUT_FD, &buffer[0], count) < 0) {
            io_close(descriptor);
            return 3;
        }

        total += count;
    }

    if (io_close(descriptor) < 0) {
        return 4;
    }

    println("bytes={}", total);
    return 0;
}

Execution result:

Wave
bytes=5

The buffer capacity is 4, but the last read may be 1 byte. We always pass the actual count to the output. If you write the entire array, even old, unread bytes can be output.

The program opened descriptor of input.txt, so close it. Standard output is not a newly acquired resource in this function, so it is not arbitrarily closed at the end of the example.

Full reads and insufficient capacity

read_into receives a fixed storage space that holds the entire file. If space is insufficient, it silently truncates and returns NO_SPACE without success.

import("std::fs::file")::{read_into};
import("std::io::consts")::{IO_ERR_NO_SPACE};

fun main() {
    var data: array<u8, 2>;
    var status: i64 = read_into("input.txt", &data[0], 2);

    if (status == IO_ERR_NO_SPACE) {
        println("destination too small");
    }
}

Execution result:

destination too small

It does not assume that the failed read did not change the destination byte at all. Do not use it as the finished file content, prepare a larger repository or choose the streaming method. Even if you query the size first, the actual read result is the final judgment, as the file may change between query and read.

Difference between writing and appending files

write replaces the existing content and append is added to the end. The number of bytes to store can be obtained directly from the string length and passed. The NUL at the end of the string is usually not included in the text file contents.

import("std::fs::file")::{write, append, size, remove};
import("std::string::len")::{len};

fun main() -> i32 {
    var first: str = "Wave";
    var second: str = " study";

    if (write("output.txt", first as ptr<u8>, len(first) as i64) < 0) {
        return 1;
    }

    if (append("output.txt", second as ptr<u8>, len(second) as i64) < 0) {
        return 2;
    }

    println("bytes={}", size("output.txt"));

    if (remove("output.txt") < 0) {
        return 3;
    }

    return 0;
}

Execution result:

bytes=10

This example creates, replaces, and finally deletes output.txt in the working directory. Run from the practice directory with no existing files. The actual editor or storage program may require separate storage policies, such as temporary files and replacement.

API Selection table

situation select
Read small entire file into fixed buffer read_into
Keep entire contents without knowing the size read_to_end and Buffer
Process content in order rather than storing it in its entirety open_read + io_read repeat
Read records of fixed length io_read_exact
Transmit entire byte string io_write_all
Handling already open files fd function instead of path function

After selecting a function, check how the buffer, file location, and external data change in the event of a failure.