251 lines
6.8 KiB
C++
Executable File
251 lines
6.8 KiB
C++
Executable File
export module coral.io;
|
|
|
|
import coral;
|
|
|
|
export namespace coral {
|
|
/**
|
|
* Multiplexing byte-based ring buffer that may be used for memory-backed I/O operations and fast data (re)-
|
|
* construction.
|
|
*/
|
|
struct ring_buffer : public writer, public reader {
|
|
/**
|
|
* Potential results from attempting to allocate a ring buffer dynamically.
|
|
*
|
|
* [allocate_result::ok] means nothing went wrong and the allocation was successful.
|
|
*
|
|
* [allocate_result::out_of_memory] indicates that there is not enough memory available in the provided
|
|
* allocator to create a buffer of the requested size.
|
|
*/
|
|
enum class [[nodiscard]] allocate_result {
|
|
ok,
|
|
out_of_memory,
|
|
};
|
|
|
|
ring_buffer() = default;
|
|
|
|
ring_buffer(ring_buffer const &) = delete;
|
|
|
|
~ring_buffer() override {
|
|
if (this->data_allocator != nullptr) this->data_allocator->deallocate(this->data.pointer);
|
|
}
|
|
|
|
/**
|
|
* Attempts to allocate memory for the ring buffer `capacity` number of bytes large with `data_allocator` as the
|
|
* allocation strategy.
|
|
*
|
|
* See [allocate_result] for more information on error handling.
|
|
*
|
|
* *Note*: calling allocate will wipe out the contents of the current buffer.
|
|
*/
|
|
allocate_result allocate(allocator & data_allocator, usize capacity) {
|
|
if (this->data_allocator != nullptr) this->data_allocator->deallocate(this->data.pointer);
|
|
|
|
this->clear();
|
|
|
|
u8 * const data {this->data_allocator->reallocate(nullptr, capacity)};
|
|
|
|
if (data == nullptr) {
|
|
this->data_allocator = nullptr;
|
|
this->data = {};
|
|
|
|
return allocate_result::out_of_memory;
|
|
}
|
|
|
|
this->data_allocator = &data_allocator;
|
|
this->data = {data, capacity};
|
|
|
|
return allocate_result::ok;
|
|
}
|
|
|
|
/**
|
|
* Returns a mutable [slice] ranging from the head to the last-filled element.
|
|
*
|
|
* *Note*: The lifetime and validity of the returned slice is only guaranteed for as long as the source
|
|
* [ring_buffer] is not mutated or out-of-scope.
|
|
*/
|
|
slice<u8 const> as_slice() const {
|
|
return this->data;
|
|
}
|
|
|
|
/**
|
|
* Clears all written elements from the buffer.
|
|
*/
|
|
void clear() {
|
|
this->data_filled = 0;
|
|
this->read_index = 0;
|
|
this->write_index = 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of bytes in the buffer that have been filled with data.
|
|
*/
|
|
usize filled() const {
|
|
return this->data_filled;
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if the buffer is completely empty of data, otherwise `false`.
|
|
*/
|
|
bool is_empty() const {
|
|
return this->data_filled == this->data.length;
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if the buffer has been completely filled with data, otherwise `false`.
|
|
*/
|
|
bool is_full() const {
|
|
return this->data_filled == this->data.length;
|
|
}
|
|
|
|
/**
|
|
* Attempts to write the single value `data` into the buffer, returning `true` if successful, otherwise `false`
|
|
* if the buffer is full.
|
|
*/
|
|
bool put(u8 data) {
|
|
if (this->is_full()) return false;
|
|
|
|
this->data_filled += 1;
|
|
this->data[this->write_index] = data;
|
|
this->write_index = (this->write_index + 1) % this->data.length;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reads whatever data is in the buffer into `data`, returning the number of bytes read from the buffer.
|
|
*/
|
|
expected<usize, io_error> read(slice<u8> const & data) override {
|
|
slice const readable_data {this->data.sliced(0, min(this->data_filled, data.length))};
|
|
|
|
this->data_filled -= readable_data.length;
|
|
|
|
for (usize index = 0; index < readable_data.length; index += 1) {
|
|
data[index] = this->data[this->read_index];
|
|
this->read_index = (this->read_index + 1) % this->data.length;
|
|
}
|
|
|
|
return readable_data.length;
|
|
}
|
|
|
|
/**
|
|
* Returns the remaining unfilled buffer space in bytes.
|
|
*/
|
|
usize remaining() const {
|
|
return this->data.length - this->data_filled;
|
|
}
|
|
|
|
/**
|
|
* Write `data` to the buffer, returning the number of bytes written to the buffer.
|
|
*/
|
|
expected<usize, io_error> write(slice<u8 const> const & data) override {
|
|
slice const writable_data {data.sliced(0, min(data.length, this->remaining()))};
|
|
|
|
this->data_filled += writable_data.length;
|
|
|
|
for (usize index = 0; index < writable_data.length; index += 1) {
|
|
this->data[this->write_index] = data[index];
|
|
this->write_index = (this->write_index + 1) % this->data.length;
|
|
}
|
|
|
|
return writable_data.length;
|
|
}
|
|
|
|
private:
|
|
usize data_filled {0};
|
|
|
|
usize read_index {0};
|
|
|
|
usize write_index {0};
|
|
|
|
allocator * data_allocator {nullptr};
|
|
|
|
slice<u8> data {};
|
|
};
|
|
|
|
/**
|
|
* Streams the data from `input` to `output`, using `buffer` as temporary transfer space.
|
|
*
|
|
* The returned [expected] can be used to introspect if `input` or `output` encountered any issues during streaming,
|
|
* otherwise it will contain the number of bytes streamed.
|
|
*
|
|
* *Note*: if `buffer` has a length of `0`, no data will be streamed as there is nowhere to temporarily place data
|
|
* during streaming.
|
|
*/
|
|
expected<u64, io_error> stream(writer & output, reader & input, slice<u8> const & buffer) {
|
|
u64 total_bytes_written {0};
|
|
expected bytes_read {input.read(buffer)};
|
|
|
|
if (bytes_read.is_error()) return *bytes_read.error();
|
|
|
|
usize read {*bytes_read.ok()};
|
|
|
|
while (read != 0) {
|
|
expected const bytes_written = output.write(buffer.sliced(0, read));
|
|
|
|
if (bytes_written.is_error()) return *bytes_read.error();
|
|
|
|
total_bytes_written += *bytes_written.ok();
|
|
bytes_read = input.read(buffer);
|
|
|
|
if (bytes_read.is_error()) return *bytes_read.error();
|
|
|
|
read = *bytes_read.ok();
|
|
}
|
|
|
|
return total_bytes_written;
|
|
}
|
|
|
|
/**
|
|
* Returns a reference to a shared [allocator] which will always return `nullptr` on calls to
|
|
* [allocator::reallocate].
|
|
*/
|
|
allocator & null_allocator() {
|
|
static struct : public allocator {
|
|
u8 * reallocate(u8 * maybe_allocation, usize requested_size) override {
|
|
if (maybe_allocation != nullptr) unreachable();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void deallocate(void * allocation) override {
|
|
if (allocation != nullptr) unreachable();
|
|
}
|
|
} a;
|
|
|
|
return a;
|
|
}
|
|
|
|
/**
|
|
* Attempts to format and print `value` as an unsigned integer out to `output`.
|
|
*
|
|
* The returned [expected] can be used to introspect if `output` encountered any issues during printing, otherwise
|
|
* it will contain the number of characters used to print `value` as text.
|
|
*/
|
|
expected<usize, io_error> print_unsigned(writer & output, u64 value) {
|
|
if (value == 0) return output.write(slice{"0"}.as_bytes());
|
|
|
|
u8 buffer[20]{0};
|
|
usize buffer_count{0};
|
|
|
|
while (value != 0) {
|
|
constexpr usize radix{10};
|
|
|
|
buffer[buffer_count] = static_cast<u8>((value % radix) + '0');
|
|
value = (value / radix);
|
|
buffer_count += 1;
|
|
}
|
|
|
|
usize const half_buffer_count{buffer_count / 2};
|
|
|
|
for (usize i = 0; i < half_buffer_count; i += 1)
|
|
swap(buffer[i], buffer[buffer_count - i - 1]);
|
|
|
|
return output.write({buffer, buffer_count});
|
|
}
|
|
|
|
template<typename... arguments> expected<usize, io_error> println(writer & output, arguments... print_arguments) {
|
|
// TODO: Implement.
|
|
return 0;
|
|
}
|
|
}
|