diff --git a/vlib/os/file_buffering.c.v b/vlib/os/file_buffering.c.v new file mode 100644 index 0000000000..02d44fc23e --- /dev/null +++ b/vlib/os/file_buffering.c.v @@ -0,0 +1,42 @@ +module os + +fn C.setvbuf(stream &C.FILE, buffer &char, mode int, size usize) int + +// FileBufferMode describes the available buffering modes for an os.File: unbuffered, line buffered and block/fully buffered. +// Normally all files are block buffered. If a stream refers to a terminal (as stdout normally does), it is line buffered. +// The standard error stream stderr is always unbuffered by default. +pub enum FileBufferMode { + fully_buffered = C._IOFBF // many characters are saved up and written as a block + line_buffered = C._IOLBF // characters are saved up until a newline is output or input is read from any stream attached to a terminal device (typically stdin) + not_buffered = C._IONBF // information appears on the destination file or terminal as soon as it is written +} + +// setvbuf sets the buffer and buffering mode for the given file `f`. See also os.FileBufferMode. +// It returns 0 on success. It returns nonzero on failure (the mode is invalid, or the request cannot be honored). +// Except for unbuffered files, the buffer argument should point to a buffer at least size bytes long; this buffer will be used instead of the current buffer. +// If the argument buffer is nil, only the mode is affected; a new buffer will be allocated on the next read or write operation. +// Note: make sure, that the space, that buffer points to (when != 0), still exists by the time the file stream is closed, which also happens at program termination. +// Note: f.setvbuf() may be used only after opening a file stream, and before any other operations have been performed on it. +@[unsafe] +pub fn (mut f File) setvbuf(buffer &char, mode FileBufferMode, size usize) int { + return C.setvbuf(f.cfile, buffer, int(mode), size) +} + +// set_buffer sets the buffer for the file, and the file buffering mode (see also os.FileBufferMode). +// Unlike File.setvbuf, it allows you to pass an existing V []u8 array directly. +// Note: f.set_buffer() may be used only after opening a file stream, and before any other operations have been performed on it. +pub fn (mut f File) set_buffer(mut buffer []u8, mode FileBufferMode) int { + return unsafe { f.setvbuf(&char(buffer.data), mode, usize(buffer.len)) } +} + +// set_line_buffered sets the file buffering mode to FileBufferMode.line_buffered. +// Note: f.set_line_buffered() may be used only after opening a file stream, and before any other operations have been performed on it. +pub fn (mut f File) set_line_buffered() { + unsafe { f.setvbuf(&char(nil), .line_buffered, usize(0)) } +} + +// set_unbuffered sets the file buffering mode to FileBufferMode.not_buffered. +// Note: f.set_unbuffered() may be used only after opening a file stream, and before any other operations have been performed on it. +pub fn (mut f File) set_unbuffered() { + unsafe { f.setvbuf(&char(nil), .not_buffered, usize(0)) } +} diff --git a/vlib/os/file_buffering_test.v b/vlib/os/file_buffering_test.v new file mode 100644 index 0000000000..0a1e24f78b --- /dev/null +++ b/vlib/os/file_buffering_test.v @@ -0,0 +1,87 @@ +import os + +const tfolder = os.join_path(os.vtmp_dir(), 'tests', 'os_file_buffering_test') + +fn testsuite_begin() { + os.rmdir_all(tfolder) or {} + assert !os.is_dir(tfolder) + os.mkdir_all(tfolder)! + os.chdir(tfolder)! + assert os.is_dir(tfolder) +} + +fn testsuite_end() { + os.rmdir_all(tfolder) or {} +} + +fn test_set_buffer_line_buffered() { + dump(@LOCATION) + mut buf := []u8{len: 25} + dump(buf) + mut wfile := os.open_file('text.txt', 'wb', 0o666)! + wfile.set_buffer(mut buf, .line_buffered) + wfile.write_string('----------------------------------\n')! + for line in ['hello\n', 'world\n', 'hi\n'] { + wfile.write_string(line)! + wfile.flush() + dump(buf) + print(buf.bytestr()) + // assert buf.bytestr().contains(line) // this works on GLIBC, but fails on MUSL. + unsafe { buf.reset() } + } + wfile.close() + // + content := os.read_lines('text.txt')! + dump(content) + assert content == ['----------------------------------', 'hello', 'world', 'hi'] +} + +fn test_set_buffer_fully_buffered() { + dump(@LOCATION) + mut buf := []u8{len: 30} + dump(buf) + mut wfile := os.open_file('text.txt', 'wb', 0o666)! + wfile.set_buffer(mut buf, .fully_buffered) + // Ubuntu GLIBC 2.31 seems to not use the buffer for the first write call, but it does write to the buffer first for the subsequent ones. + // MUSL (detecting the MUSL version is deliberately made hard by its authors, because of course it is :-( ...), will skip the first 8 bytes + // of the buffer, and write everything after those. + wfile.write_string('S')! + wfile.write_string('---\n')! + dump(buf) + for line in ['hello\n', 'world\n', 'hi\n'] { + wfile.write_string(line)! + dump(buf) + // print(buf.bytestr()) + } + wfile.close() + dump(buf) + // assert buf.bytestr().starts_with('---\nhello\nworld\nhi\n') // works on GLIBC, fails on MUSL + assert buf.bytestr().contains('---\nhello\nworld\n') + // + content := os.read_lines('text.txt')! + dump(content) + assert content == ['S---', 'hello', 'world', 'hi'] +} + +fn test_set_unbuffered() { + dump(@LOCATION) + mut buf := []u8{len: 30} + dump(buf) + mut wfile := os.open_file('text.txt', 'wb', 0o666)! + wfile.set_buffer(mut buf, .not_buffered) + wfile.write_string('S')! + wfile.write_string('---\n')! + dump(buf) + for line in ['hello\n', 'world\n', 'hi\n'] { + wfile.write_string(line)! + dump(buf) + // print(buf.bytestr()) + } + wfile.close() + // dump(buf.bytestr()) + assert buf.all(it == 0) + // + content := os.read_lines('text.txt')! + dump(content) + assert content == ['S---', 'hello', 'world', 'hi'] +}