diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt --- a/libc/config/linux/x86_64/entrypoints.txt +++ b/libc/config/linux/x86_64/entrypoints.txt @@ -393,6 +393,8 @@ libc.src.stdio.putc libc.src.stdio.putchar libc.src.stdio.puts + libc.src.stdio.setbuf + libc.src.stdio.setvbuf libc.src.stdio.stderr libc.src.stdio.stdin libc.src.stdio.stdout diff --git a/libc/spec/stdc.td b/libc/spec/stdc.td --- a/libc/spec/stdc.td +++ b/libc/spec/stdc.td @@ -613,6 +613,16 @@ RetValSpec, [ArgSpec] >, + FunctionSpec< + "setbuf", + RetValSpec, + [ArgSpec, ArgSpec] + >, + FunctionSpec< + "setvbuf", + RetValSpec, + [ArgSpec, ArgSpec, ArgSpec, ArgSpec] + >, FunctionSpec< "sprintf", RetValSpec, diff --git a/libc/src/__support/File/file.h b/libc/src/__support/File/file.h --- a/libc/src/__support/File/file.h +++ b/libc/src/__support/File/file.h @@ -71,6 +71,11 @@ Mutex mutex; + // For files which are readable, we should be able to support one ungetc + // operation even if |buf| is nullptr. So, in the constructor of File, we + // set |buf| to point to this buffer character. + char ungetc_buf; + void *buf; // Pointer to the stream buffer for buffered streams size_t bufsize; // Size of the buffer pointed to by |buf|. @@ -111,13 +116,13 @@ }; protected: - bool write_allowed() const { + constexpr bool write_allowed() const { return mode & (static_cast(OpenMode::WRITE) | static_cast(OpenMode::APPEND) | static_cast(OpenMode::PLUS)); } - bool read_allowed() const { + constexpr bool read_allowed() const { return mode & (static_cast(OpenMode::READ) | static_cast(OpenMode::PLUS)); } @@ -125,15 +130,21 @@ public: // We want this constructor to be constexpr so that global file objects // like stdout do not require invocation of the constructor which can - // potentially lead to static initialization order fiasco. + // potentially lead to static initialization order fiasco. Consequently, + // we will assume that the |buffer| and |buffer_size| argument are + // meaningful - that is, |buffer| is nullptr if and only if |buffer_size| + // is zero. This way, we will not have to employ the semantics of + // the set_buffer method and allocate a buffer. constexpr File(WriteFunc *wf, ReadFunc *rf, SeekFunc *sf, CloseFunc *cf, FlushFunc *ff, void *buffer, size_t buffer_size, int buffer_mode, bool owned, ModeFlags modeflags) : platform_write(wf), platform_read(rf), platform_seek(sf), platform_close(cf), platform_flush(ff), mutex(false, false, false), - buf(buffer), bufsize(buffer_size), bufmode(buffer_mode), own_buf(owned), - mode(modeflags), pos(0), prev_op(FileOp::NONE), read_limit(0), - eof(false), err(false) {} + ungetc_buf(0), buf(buffer), bufsize(buffer_size), bufmode(buffer_mode), + own_buf(owned), mode(modeflags), pos(0), prev_op(FileOp::NONE), + read_limit(0), eof(false), err(false) { + adjust_buf(); + } // This function helps initialize the various fields of the File data // structure after a allocating memory for it via a call to malloc. @@ -156,6 +167,8 @@ f->prev_op = FileOp::NONE; f->read_limit = f->pos = 0; f->eof = f->err = false; + + f->adjust_buf(); } // Buffered write of |len| bytes from |data| without the file lock. @@ -196,9 +209,16 @@ } // Sets the internal buffer to |buffer| with buffering mode |mode|. - // |size| is the size of |buffer|. This new |buffer| is owned by the - // stream only if |owned| is true. - void set_buffer(void *buffer, size_t size, bool owned); + // |size| is the size of |buffer|. If |size| is non-zero, but |buffer| + // is nullptr, then a buffer owned by this file will be allocated. + // Else, |buffer| will not be owned by this file. + // + // Will return zero on success, or an error value on failure. Will fail + // if: + // 1. |buffer| is not a nullptr but |size| is zero. + // 2. |buffer_mode| is not one of _IOLBF, IOFBF or _IONBF. + // In both the above cases, error returned in EINVAL. + int set_buffer(void *buffer, size_t size, int buffer_mode); // Closes the file stream and frees up all resources owned by it. int close(); @@ -235,6 +255,28 @@ size_t write_unlocked_lbf(const uint8_t *data, size_t len); size_t write_unlocked_fbf(const uint8_t *data, size_t len); size_t write_unlocked_nbf(const uint8_t *data, size_t len); + + constexpr void adjust_buf() { + if (read_allowed() && (buf == nullptr || bufsize == 0)) { + // We should allow atleast one ungetc operation. + // This might give an impression that a buffer will be used even when + // the user does not want a buffer. But, that will not be the case. + // For reading, the buffering does not come into play. For writing, let + // us take up the three different kinds of buffering separately: + // 1. If user wants _IOFBF but gives a zero buffer, buffering still + // happens in the OS layer until the user flushes. So, from the user's + // point of view, this single byte buffer does not affect their + // experience. + // 2. If user wants _IOLBF but gives a zero buffer, the reasoning is + // very similar to the _IOFBF case. + // 3. If user wants _IONBF, then the buffer is ignored for writing. + // So, all of the above cases, having a single ungetc buffer does not + // affect the behavior experienced by the user. + buf = &ungetc_buf; + bufsize = 1; + own_buf = false; // We shouldn't call free on |buf| when closing the file. + } + } }; // The implementaiton of this function is provided by the platfrom_file diff --git a/libc/src/__support/File/file.cpp b/libc/src/__support/File/file.cpp --- a/libc/src/__support/File/file.cpp +++ b/libc/src/__support/File/file.cpp @@ -330,12 +330,49 @@ return 0; } -void File::set_buffer(void *buffer, size_t size, bool owned) { - if (own_buf) - free(buf); - buf = static_cast(buffer); - bufsize = size; - own_buf = owned; +int File::set_buffer(void *buffer, size_t size, int buffer_mode) { + // We do not need to lock the file as this method should be called before + // other operations are performed on the file. + + if (buffer != nullptr && size == 0) + return EINVAL; + + switch (buffer_mode) { + case _IOFBF: + case _IOLBF: + case _IONBF: + break; + default: + return EINVAL; + } + + if (buffer == nullptr && size != 0 && buffer_mode != _IONBF) { + // We exclude the case of buffer_mode == _IONBF in this branch + // because we don't need to allocate buffer in such a case. + if (own_buf) { + buf = realloc(buf, size); + } else { + buf = malloc(size); + own_buf = true; + } + bufsize = size; + // TODO: Handle allocation failures. + } else { + if (own_buf) + free(buf); + if (buffer_mode != _IONBF) { + buf = static_cast(buffer); + bufsize = size; + } else { + // We don't need any buffer. + buf = nullptr; + bufsize = 0; + } + own_buf = false; + } + bufmode = buffer_mode; + adjust_buf(); + return 0; } File::ModeFlags File::mode_flags(const char *mode) { diff --git a/libc/src/stdio/CMakeLists.txt b/libc/src/stdio/CMakeLists.txt --- a/libc/src/stdio/CMakeLists.txt +++ b/libc/src/stdio/CMakeLists.txt @@ -341,6 +341,32 @@ libc.src.__support.File.platform_file ) +add_entrypoint_object( + setbuf + SRCS + setbuf.cpp + HDRS + setbuf.h + DEPENDS + libc.include.errno + libc.include.stdio + libc.src.__support.File.file + libc.src.__support.File.platform_file +) + +add_entrypoint_object( + setvbuf + SRCS + setvbuf.cpp + HDRS + setvbuf.h + DEPENDS + libc.include.errno + libc.include.stdio + libc.src.__support.File.file + libc.src.__support.File.platform_file +) + add_entrypoint_object( sprintf SRCS diff --git a/libc/src/stdio/setbuf.h b/libc/src/stdio/setbuf.h new file mode 100644 --- /dev/null +++ b/libc/src/stdio/setbuf.h @@ -0,0 +1,20 @@ +//===-- Implementation header of setbuf -------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_SRC_STDIO_SETBUF_H +#define LLVM_LIBC_SRC_STDIO_SETBUF_H + +#include + +namespace __llvm_libc { + +void setbuf(::FILE *__restrict stream, char *__restrict buf); + +} // namespace __llvm_libc + +#endif // LLVM_LIBC_SRC_STDIO_SETBUF_H diff --git a/libc/src/stdio/setbuf.cpp b/libc/src/stdio/setbuf.cpp new file mode 100644 --- /dev/null +++ b/libc/src/stdio/setbuf.cpp @@ -0,0 +1,28 @@ +//===-- Implementation of setbuf ------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/stdio/setbuf.h" +#include "src/__support/File/file.h" + +#include +#include + +namespace __llvm_libc { + +LLVM_LIBC_FUNCTION(void, setbuf, + (::FILE *__restrict stream, char *__restrict buf)) { + int mode = _IOFBF; + if (buf == nullptr) + mode = _IONBF; + int err = reinterpret_cast<__llvm_libc::File *>(stream)->set_buffer( + buf, BUFSIZ, mode); + if (err != 0) + errno = err; +} + +} // namespace __llvm_libc diff --git a/libc/src/stdio/setvbuf.h b/libc/src/stdio/setvbuf.h new file mode 100644 --- /dev/null +++ b/libc/src/stdio/setvbuf.h @@ -0,0 +1,21 @@ +//===-- Implementation header of setvbuf ------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_SRC_STDIO_SETVBUF_H +#define LLVM_LIBC_SRC_STDIO_SETVBUF_H + +#include + +namespace __llvm_libc { + +int setvbuf(::FILE *__restrict stream, char *__restrict buf, int type, + size_t size); + +} // namespace __llvm_libc + +#endif // LLVM_LIBC_SRC_STDIO_SETVBUF_H diff --git a/libc/src/stdio/setvbuf.cpp b/libc/src/stdio/setvbuf.cpp new file mode 100644 --- /dev/null +++ b/libc/src/stdio/setvbuf.cpp @@ -0,0 +1,27 @@ +//===-- Implementation of setvbuf -----------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/stdio/setvbuf.h" +#include "src/__support/File/file.h" + +#include +#include + +namespace __llvm_libc { + +LLVM_LIBC_FUNCTION(int, setvbuf, + (::FILE *__restrict stream, char *__restrict buf, int type, + size_t size)) { + int err = reinterpret_cast<__llvm_libc::File *>(stream)->set_buffer(buf, size, + type); + if (err != 0) + errno = err; + return err; +} + +} // namespace __llvm_libc diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt --- a/libc/test/src/stdio/CMakeLists.txt +++ b/libc/test/src/stdio/CMakeLists.txt @@ -37,6 +37,38 @@ libc.src.stdio.ungetc ) +add_libc_unittest( + setbuf_test + SUITE + libc_stdio_unittests + SRCS + setbuf_test.cpp + DEPENDS + libc.include.stdio + libc.src.stdio.fclose + libc.src.stdio.fopen + libc.src.stdio.fread + libc.src.stdio.fwrite + libc.src.stdio.setbuf + libc.src.stdio.ungetc +) + +add_libc_unittest( + setvbuf_test + SUITE + libc_stdio_unittests + SRCS + setvbuf_test.cpp + DEPENDS + libc.include.errno + libc.include.stdio + libc.src.stdio.fclose + libc.src.stdio.fopen + libc.src.stdio.fread + libc.src.stdio.fwrite + libc.src.stdio.setvbuf +) + add_libc_unittest( unlocked_fileop_test SUITE diff --git a/libc/test/src/stdio/setbuf_test.cpp b/libc/test/src/stdio/setbuf_test.cpp new file mode 100644 --- /dev/null +++ b/libc/test/src/stdio/setbuf_test.cpp @@ -0,0 +1,68 @@ +//===-- Unittests for setbuf ----------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/stdio/fclose.h" +#include "src/stdio/fopen.h" +#include "src/stdio/fread.h" +#include "src/stdio/fwrite.h" +#include "src/stdio/setbuf.h" +#include "src/stdio/ungetc.h" +#include "utils/UnitTest/Test.h" + +#include + +TEST(LlvmLibcSetbufTest, DefaultBufsize) { + // The idea in this test is to change the buffer after opening a file and + // ensure that read and write work as expected. + constexpr char FILENAME[] = "testdata/setbuf_test_default_bufsize.test"; + ::FILE *file = __llvm_libc::fopen(FILENAME, "w"); + ASSERT_FALSE(file == nullptr); + char buffer[BUFSIZ]; + __llvm_libc::setbuf(file, buffer); + constexpr char CONTENT[] = "abcdef"; + constexpr size_t CONTENT_SIZE = sizeof(CONTENT); + ASSERT_EQ(CONTENT_SIZE, __llvm_libc::fwrite(CONTENT, 1, CONTENT_SIZE, file)); + ASSERT_EQ(0, __llvm_libc::fclose(file)); + + file = __llvm_libc::fopen(FILENAME, "r"); + __llvm_libc::setbuf(file, buffer); + ASSERT_FALSE(file == nullptr); + char data[CONTENT_SIZE]; + ASSERT_EQ(__llvm_libc::fread(&data, 1, CONTENT_SIZE, file), CONTENT_SIZE); + ASSERT_STREQ(CONTENT, data); + ASSERT_EQ(0, __llvm_libc::fclose(file)); +} + +TEST(LlvmLibcSetbufTest, NullBuffer) { + // The idea in this test is that we set a null buffer and ensure that + // everything works correctly. + constexpr char FILENAME[] = "testdata/setbuf_test_null_buffer.test"; + ::FILE *file = __llvm_libc::fopen(FILENAME, "w"); + ASSERT_FALSE(file == nullptr); + __llvm_libc::setbuf(file, nullptr); + constexpr char CONTENT[] = "abcdef"; + constexpr size_t CONTENT_SIZE = sizeof(CONTENT); + ASSERT_EQ(CONTENT_SIZE, __llvm_libc::fwrite(CONTENT, 1, CONTENT_SIZE, file)); + ASSERT_EQ(0, __llvm_libc::fclose(file)); + + file = __llvm_libc::fopen(FILENAME, "r"); + __llvm_libc::setbuf(file, nullptr); + ASSERT_FALSE(file == nullptr); + char data[CONTENT_SIZE]; + ASSERT_EQ(__llvm_libc::fread(&data, 1, CONTENT_SIZE, file), CONTENT_SIZE); + ASSERT_STREQ(CONTENT, data); + + // Ensure that ungetc also works. + char unget_char = 'z'; + ASSERT_EQ(int(unget_char), __llvm_libc::ungetc(unget_char, file)); + char c; + ASSERT_EQ(__llvm_libc::fread(&c, 1, 1, file), size_t(1)); + ASSERT_EQ(c, unget_char); + + ASSERT_EQ(0, __llvm_libc::fclose(file)); +} diff --git a/libc/test/src/stdio/setvbuf_test.cpp b/libc/test/src/stdio/setvbuf_test.cpp new file mode 100644 --- /dev/null +++ b/libc/test/src/stdio/setvbuf_test.cpp @@ -0,0 +1,106 @@ +//===-- Unittests for setvbuf ---------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/stdio/fclose.h" +#include "src/stdio/fopen.h" +#include "src/stdio/fread.h" +#include "src/stdio/fwrite.h" +#include "src/stdio/setvbuf.h" +#include "utils/UnitTest/Test.h" + +#include +#include + +TEST(LlvmLibcSetvbufTest, SetNBFBuffer) { + // The idea in this test is that we open a file for writing and reading, and + // then set a NBF buffer to the write handle. Since it is NBF, the data + // written using the write handle should be immediately readable by the read + // handle. + constexpr char FILENAME[] = "testdata/setvbuf_nbf.test"; + + ::FILE *fw = __llvm_libc::fopen(FILENAME, "w"); + ASSERT_FALSE(fw == nullptr); + char buffer[BUFSIZ]; + ASSERT_EQ(__llvm_libc::setvbuf(fw, buffer, _IONBF, BUFSIZ), 0); + + ::FILE *fr = __llvm_libc::fopen(FILENAME, "r"); + ASSERT_FALSE(fr == nullptr); + + constexpr char CONTENT[] = "abcdef"; + constexpr size_t CONTENT_SIZE = sizeof(CONTENT); + for (size_t i = 0; i < CONTENT_SIZE; ++i) { + ASSERT_EQ(size_t(1), __llvm_libc::fwrite(CONTENT + i, 1, 1, fw)); + char c; + ASSERT_EQ(size_t(1), __llvm_libc::fread(&c, 1, 1, fr)); + ASSERT_EQ(c, CONTENT[i]); + } + + ASSERT_EQ(0, __llvm_libc::fclose(fw)); + ASSERT_EQ(0, __llvm_libc::fclose(fr)); + + // Make sure NBF buffer has no effect for reading. + fr = __llvm_libc::fopen(FILENAME, "r"); + char data[CONTENT_SIZE]; + ASSERT_EQ(__llvm_libc::setvbuf(fr, buffer, _IONBF, BUFSIZ), 0); + ASSERT_EQ(CONTENT_SIZE, __llvm_libc::fread(data, 1, CONTENT_SIZE, fr)); + ASSERT_STREQ(CONTENT, data); + ASSERT_EQ(0, __llvm_libc::fclose(fr)); +} + +TEST(LlvmLibcSetvbufTest, SetLBFBuffer) { + // The idea in this test is that we open a file for writing and reading, and + // then set a LBF buffer to the write handle. Since it is LBF, the data + // written using the write handle should be available right after a '\n' is + // written. + constexpr char FILENAME[] = "testdata/setvbuf_lbf.test"; + + ::FILE *fw = __llvm_libc::fopen(FILENAME, "w"); + ASSERT_FALSE(fw == nullptr); + char buffer[BUFSIZ]; + ASSERT_EQ(__llvm_libc::setvbuf(fw, buffer, _IOLBF, BUFSIZ), 0); + + ::FILE *fr = __llvm_libc::fopen(FILENAME, "r"); + ASSERT_FALSE(fr == nullptr); + + constexpr char CONTENT[] = "abcdef\n"; + constexpr size_t CONTENT_SIZE = sizeof(CONTENT); + ASSERT_EQ(CONTENT_SIZE, __llvm_libc::fwrite(CONTENT, 1, CONTENT_SIZE, fw)); + + // Note that CONTENT_SIZE worth of data written also includes the + // null-terminator '\0'. But, since it is after the new line character, + // it should not be availabe for reading. + char data[CONTENT_SIZE]; + ASSERT_EQ(CONTENT_SIZE - 1, __llvm_libc::fread(data, 1, CONTENT_SIZE, fr)); + char c; + ASSERT_EQ(size_t(0), __llvm_libc::fread(&c, 1, 1, fr)); + + data[CONTENT_SIZE - 1] = '\0'; + ASSERT_STREQ(CONTENT, data); + + ASSERT_EQ(0, __llvm_libc::fclose(fw)); + ASSERT_EQ(0, __llvm_libc::fclose(fr)); + + // Make sure LBF buffer has no effect for reading. + fr = __llvm_libc::fopen(FILENAME, "r"); + ASSERT_EQ(__llvm_libc::setvbuf(fr, buffer, _IOLBF, BUFSIZ), 0); + ASSERT_EQ(CONTENT_SIZE, __llvm_libc::fread(data, 1, CONTENT_SIZE, fr)); + ASSERT_STREQ(CONTENT, data); + ASSERT_EQ(0, __llvm_libc::fclose(fr)); +} + +TEST(LlvmLibcSetbufTest, InvalidBufferMode) { + constexpr char FILENAME[] = "testdata/setvbuf_invalid_bufmode.test"; + ::FILE *f = __llvm_libc::fopen(FILENAME, "w"); + ASSERT_FALSE(f == nullptr); + char buf[BUFSIZ]; + ASSERT_NE(__llvm_libc::setvbuf(f, buf, _IOFBF + _IOLBF + _IONBF, BUFSIZ), 0); + ASSERT_EQ(errno, EINVAL); + + errno = 0; + ASSERT_EQ(0, __llvm_libc::fclose(f)); +} diff --git a/libc/utils/HdrGen/PrototypeTestGen/PrototypeTestGen.cpp b/libc/utils/HdrGen/PrototypeTestGen/PrototypeTestGen.cpp --- a/libc/utils/HdrGen/PrototypeTestGen/PrototypeTestGen.cpp +++ b/libc/utils/HdrGen/PrototypeTestGen/PrototypeTestGen.cpp @@ -94,6 +94,7 @@ // We provide dummy malloc and free implementations to support the case // when LLVM libc does to include them. OS << "void *malloc(size_t) { return nullptr; }\n"; + OS << "void *realloc(void *, size_t) { return nullptr; }\n"; OS << "void free(void *) {}\n"; return false;