mirror of
https://github.com/vimpunk/mio.git
synced 2025-12-06 08:46:51 +08:00
API changes, windows support, bugfixes
This commit is contained in:
parent
cede64f865
commit
3a8722c27e
@ -3,6 +3,7 @@
|
||||
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#ifdef _WIN32
|
||||
# ifndef WIN32_LEAN_AND_MEAN
|
||||
@ -71,25 +72,10 @@ public:
|
||||
mmap& operator=(mmap&&);
|
||||
~mmap();
|
||||
|
||||
/**
|
||||
* On *nix systems is_open and is_mapped are the same and don't actually say if
|
||||
* the file itself is open or closed, they only refer to the mapping. This is
|
||||
* because a mapping remains valid (as long as it's not unmapped) even if another
|
||||
* entity closes the file which is being mapped.
|
||||
*
|
||||
* On Windows, however, in order to map a file, both an active file handle and a
|
||||
* mapping handle is required, so is_open checks for a valid file handle, while
|
||||
* is_mapped checks for a valid mapping handle.
|
||||
*/
|
||||
bool is_open() const noexcept;
|
||||
bool is_mapped() const noexcept;
|
||||
bool empty() const noexcept { return length() == 0; }
|
||||
|
||||
/**
|
||||
* size/length returns the logical length (i.e. the one user requested), while
|
||||
* mapped_length returns the actual mapped length, which is usually a multiple of
|
||||
* the OS' page size.
|
||||
*/
|
||||
size_type size() const noexcept { return length_; }
|
||||
size_type length() const noexcept { return length_; }
|
||||
size_type mapped_length() const noexcept { return mapped_length_; }
|
||||
@ -116,8 +102,6 @@ public:
|
||||
reference operator[](const size_type i) noexcept { return data_[i]; }
|
||||
const_reference operator[](const size_type i) const noexcept { return data_[i]; }
|
||||
|
||||
void map(const std::string& path, const size_type offset, const size_type length,
|
||||
const access_mode mode, std::error_code& error);
|
||||
void map(const handle_type handle, const size_type offset, const size_type length,
|
||||
const access_mode mode, std::error_code& error);
|
||||
void unmap();
|
||||
@ -131,9 +115,6 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
static handle_type open_file(const std::string& path,
|
||||
const access_mode mode, std::error_code& error);
|
||||
|
||||
pointer get_mapping_start() noexcept;
|
||||
|
||||
/** NOTE: file_handle_ must be valid. */
|
||||
@ -145,6 +126,10 @@ private:
|
||||
void verify_file_handle(std::error_code& error) const noexcept;
|
||||
};
|
||||
|
||||
template<typename Path>
|
||||
mmap::handle_type open_file(const Path& path,
|
||||
const mmap::access_mode mode, std::error_code& error);
|
||||
|
||||
} // namespace detail
|
||||
} // namespace mio
|
||||
|
||||
|
||||
@ -4,12 +4,15 @@
|
||||
#include "mmap_impl.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
|
||||
#ifndef _WIN32
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <sys/stat.h>
|
||||
# include <cassert>
|
||||
# include <cstdint>
|
||||
#endif
|
||||
|
||||
namespace mio {
|
||||
@ -105,45 +108,6 @@ inline mmap& mmap::operator=(mmap&& other)
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline void mmap::map(const std::string& path, const size_type offset,
|
||||
const size_type length, const access_mode mode, std::error_code& error)
|
||||
{
|
||||
if(!path.empty() && !is_open())
|
||||
{
|
||||
error.clear();
|
||||
const auto handle = open_file(path, mode, error);
|
||||
if(error) { return; }
|
||||
map(handle, offset, length, mode, error);
|
||||
}
|
||||
}
|
||||
|
||||
inline mmap::handle_type mmap::open_file(const std::string& path,
|
||||
const mmap::access_mode mode, std::error_code& error)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const auto handle = ::CreateFile(path.c_str(),
|
||||
mode == mmap::access_mode::read_only
|
||||
? GENERIC_READ : GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
0,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
0);
|
||||
if(handle == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
error = last_error();
|
||||
}
|
||||
#else
|
||||
const auto handle = ::open(path.c_str(),
|
||||
mode == mmap::access_mode::read_only ? O_RDONLY : O_RDWR);
|
||||
if(handle == -1)
|
||||
{
|
||||
error = last_error();
|
||||
}
|
||||
#endif
|
||||
return handle;
|
||||
}
|
||||
|
||||
inline void mmap::map(const handle_type handle, const size_type offset,
|
||||
const size_type length, const access_mode mode, std::error_code& error)
|
||||
{
|
||||
@ -269,13 +233,13 @@ inline void mmap::unmap()
|
||||
inline mmap::size_type mmap::query_file_size(std::error_code& error) noexcept
|
||||
{
|
||||
#ifdef _WIN32
|
||||
PLARGE_INTEGER file_size;
|
||||
LARGE_INTEGER file_size;
|
||||
if(::GetFileSizeEx(file_handle_, &file_size) == 0)
|
||||
{
|
||||
error = last_error();
|
||||
return 0;
|
||||
}
|
||||
return file_size;
|
||||
return static_cast<size_type>(file_size.QuadPart);
|
||||
#else
|
||||
struct stat sbuf;
|
||||
if(::fstat(file_handle_, &sbuf) == -1)
|
||||
@ -345,6 +309,53 @@ inline bool operator!=(const mmap& a, const mmap& b)
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
template<
|
||||
typename String,
|
||||
typename = decltype(std::declval<String>().data()),
|
||||
typename = typename std::enable_if<!std::is_same<const char*, String>::value>::type
|
||||
> const char* c_str(const String& path)
|
||||
{
|
||||
return path.data();
|
||||
}
|
||||
|
||||
template<
|
||||
typename String,
|
||||
typename = typename std::enable_if<
|
||||
std::is_same<const char*, typename std::decay<String>::type>::value
|
||||
>::type
|
||||
> const char* c_str(String path)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
template<typename Path>
|
||||
mmap::handle_type open_file(const Path& path,
|
||||
const mmap::access_mode mode, std::error_code& error)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const auto handle = ::CreateFile(c_str(path),
|
||||
mode == mmap::access_mode::read_only
|
||||
? GENERIC_READ : GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
0,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
0);
|
||||
if(handle == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
error = last_error();
|
||||
}
|
||||
#else
|
||||
const auto handle = ::open(c_str(path),
|
||||
mode == mmap::access_mode::read_only ? O_RDONLY : O_RDWR);
|
||||
if(handle == -1)
|
||||
{
|
||||
error = last_error();
|
||||
}
|
||||
#endif
|
||||
return handle;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
} // namespace mio
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#define MIO_MMAP_HEADER
|
||||
|
||||
#include "detail/mmap_impl.hpp"
|
||||
#include <system_error>
|
||||
|
||||
namespace mio {
|
||||
|
||||
@ -10,14 +11,11 @@ namespace mio {
|
||||
* offsets that are aligned with the operating system's page granularity, this is taken
|
||||
* care of within the class in both cases.
|
||||
*
|
||||
* Both classes have std::unique_ptr<> semantics, thus only a single entity may own
|
||||
* a mapping to a file at any given time.
|
||||
* Both classes have single-ownership semantics, and transferring ownership may be
|
||||
* accomplished by moving the mmap instance.
|
||||
*
|
||||
* Remapping a file is possible, but unmap must be called before that.
|
||||
*
|
||||
* For now, both classes may only be used with an existing open file by providing the
|
||||
* file's handle.
|
||||
*
|
||||
* Both classes' destructors unmap the file. However, mmap_sink's destructor does not
|
||||
* sync the mapped file view to disk, this has to be done manually with a call tosink.
|
||||
*/
|
||||
@ -45,13 +43,18 @@ public:
|
||||
using handle_type = impl_type::handle_type;
|
||||
|
||||
mmap_source() = default;
|
||||
mmap_source(const std::string& path, const size_type offset, const size_type length)
|
||||
{
|
||||
std::error_code error;
|
||||
map(path, offset, length, error);
|
||||
if(error) { throw error; }
|
||||
}
|
||||
|
||||
/**
|
||||
* `handle` must be a valid file handle, which is then used to memory map the
|
||||
* requested region. Upon failure a `std::error_code` is thrown, detailing the
|
||||
* cause of the error, and the object remains in an unmapped state.
|
||||
*
|
||||
* When specifying `offset`, there is no need to worry about providing
|
||||
* a value that is aligned with the operating system's page allocation granularity.
|
||||
* This is adjusted by the implementation such that the first requested byte (as
|
||||
* returned by `data` or `begin`), so long as `offset` is valid, will be at `offset`
|
||||
* from the start of the file.
|
||||
*/
|
||||
mmap_source(const handle_type handle, const size_type offset, const size_type length)
|
||||
{
|
||||
std::error_code error;
|
||||
@ -60,54 +63,92 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* On *nix systems is_open and is_mapped are the same and don't actually say if
|
||||
* `mmap_source` has single-ownership semantics, so transferring ownership may only
|
||||
* be accomplished by moving the instance.
|
||||
*/
|
||||
mmap_source(const mmap_source&) = delete;
|
||||
mmap_source(mmap_source&&) = default;
|
||||
|
||||
/** The destructor invokes unmap. */
|
||||
~mmap_source() = default;
|
||||
|
||||
/**
|
||||
* On UNIX systems `is_open` and `is_mapped` are the same and don't actually say if
|
||||
* the file itself is open or closed, they only refer to the mapping. This is
|
||||
* because a mapping remains valid (as long as it's not unmapped) even if another
|
||||
* entity closes the file which is being mapped.
|
||||
*
|
||||
* On Windows, however, in order to map a file, both an active file handle and a
|
||||
* mapping handle is required, so is_open checks for a valid file handle, while
|
||||
* is_mapped checks for a valid mapping handle.
|
||||
* mapping handle is required, so `is_open` checks for a valid file handle, while
|
||||
* `is_mapped` checks for a valid mapping handle.
|
||||
*/
|
||||
bool is_open() const noexcept { return impl_.is_open(); }
|
||||
bool is_mapped() const noexcept { return impl_.is_mapped(); }
|
||||
bool empty() const noexcept { return impl_.empty(); }
|
||||
|
||||
/**
|
||||
* size/length returns the logical length (i.e. the one user requested), while
|
||||
* mapped_length returns the actual mapped length, which is usually a multiple of
|
||||
* the OS' page size.
|
||||
* `size` and `length` both return the logical length (i.e. the one user requested),
|
||||
* while `mapped_length` returns the actual mapped length, which is usually a
|
||||
* multiple of the underlying operating system's page allocation granularity.
|
||||
*/
|
||||
size_type size() const noexcept { return impl_.size(); }
|
||||
size_type length() const noexcept { return impl_.length(); }
|
||||
size_type mapped_length() const noexcept { return impl_.mapped_length(); }
|
||||
|
||||
/**
|
||||
* Returns a pointer to the first requested byte, or `nullptr` if no memory mapping
|
||||
* exists.
|
||||
*/
|
||||
const_pointer data() const noexcept { return impl_.data(); }
|
||||
|
||||
/**
|
||||
* Returns an iterator to the first requested byte, if a valid memory mapping
|
||||
* exists, otherwise this function call is equivalent to invoking `end`.
|
||||
*/
|
||||
const_iterator begin() const noexcept { return impl_.begin(); }
|
||||
const_iterator cbegin() const noexcept { return impl_.cbegin(); }
|
||||
|
||||
/** Returns an iterator one past the last requested byte. */
|
||||
const_iterator end() const noexcept { return impl_.end(); }
|
||||
const_iterator cend() const noexcept { return impl_.cend(); }
|
||||
|
||||
const_reverse_iterator rbegin() const noexcept { return impl_.rbegin(); }
|
||||
const_reverse_iterator crbegin() const noexcept { return impl_.crbegin(); }
|
||||
|
||||
const_reverse_iterator rend() const noexcept { return impl_.rend(); }
|
||||
const_reverse_iterator crend() const noexcept { return impl_.crend(); }
|
||||
|
||||
/**
|
||||
* Returns a reference to the `i`th byte from the first requested byte (as returned
|
||||
* by `data`). If this is invoked when no valid memory mapping has been established
|
||||
* prior to this call, undefined behaviour ensues.
|
||||
*/
|
||||
const_reference operator[](const size_type i) const noexcept { return impl_[i]; }
|
||||
|
||||
void map(const std::string& path, const size_type offset,
|
||||
const size_type length, std::error_code& error)
|
||||
{
|
||||
impl_.map(path, offset, length, impl_type::access_mode::read_only, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a read-only memory mapping.
|
||||
*
|
||||
* `handle` must be a valid file handle, which is then used to memory map the
|
||||
* requested region. Upon failure, `error` is set to indicate the reason and the
|
||||
* object remains in an unmapped state.
|
||||
*
|
||||
* When specifying `offset`, there is no need to worry about providing
|
||||
* a value that is aligned with the operating system's page allocation granularity.
|
||||
* This is adjusted by the implementation such that the first requested byte (as
|
||||
* returned by `data` or `begin`), so long as `offset` is valid, will be at `offset`
|
||||
* from the start of the file.
|
||||
*/
|
||||
void map(const handle_type handle, const size_type offset,
|
||||
const size_type length, std::error_code& error)
|
||||
{
|
||||
impl_.map(handle, offset, length, impl_type::access_mode::read_only, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a valid memory mapping has been established prior to this call, this call
|
||||
* instructs the kernel to unmap the memory region and dissasociate this object
|
||||
* from the file.
|
||||
*/
|
||||
void unmap() { impl_.unmap(); }
|
||||
|
||||
void swap(mmap_source& other) { impl_.swap(other.impl_); }
|
||||
@ -146,13 +187,18 @@ public:
|
||||
using handle_type = impl_type::handle_type;
|
||||
|
||||
mmap_sink() = default;
|
||||
mmap_sink(const std::string& path, const size_type offset, const size_type length)
|
||||
{
|
||||
std::error_code error;
|
||||
map(path, offset, length, error);
|
||||
if(error) { throw error; }
|
||||
}
|
||||
|
||||
/**
|
||||
* `handle` must be a valid file handle, which is then used to memory map the
|
||||
* requested region. Upon failure a `std::error_code` is thrown, detailing the
|
||||
* cause of the error, and the object remains in an unmapped state.
|
||||
*
|
||||
* When specifying `offset`, there is no need to worry about providing
|
||||
* a value that is aligned with the operating system's page allocation granularity.
|
||||
* This is adjusted by the implementation such that the first requested byte (as
|
||||
* returned by `data` or `begin`), so long as `offset` is valid, will be at `offset`
|
||||
* from the start of the file.
|
||||
*/
|
||||
mmap_sink(const handle_type handle, const size_type offset, const size_type length)
|
||||
{
|
||||
std::error_code error;
|
||||
@ -161,35 +207,58 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* On *nix systems is_open and is_mapped are the same and don't actually say if
|
||||
* `mmap_sink` has single-ownership semantics, so transferring ownership may only
|
||||
* be accomplished by moving the instance.
|
||||
*/
|
||||
mmap_sink(const mmap_sink&) = delete;
|
||||
mmap_sink(mmap_sink&&) = default;
|
||||
|
||||
/**
|
||||
* The destructor invokes unmap, but does NOT invoke `sync`. Thus, if changes have
|
||||
* been made to the mapped region, `sync` needs to be called in order to persist
|
||||
* any writes to disk.
|
||||
*/
|
||||
~mmap_sink() = default;
|
||||
|
||||
/**
|
||||
* On UNIX systems `is_open` and `is_mapped` are the same and don't actually say if
|
||||
* the file itself is open or closed, they only refer to the mapping. This is
|
||||
* because a mapping remains valid (as long as it's not unmapped) even if another
|
||||
* entity closes the file which is being mapped.
|
||||
*
|
||||
* On Windows, however, in order to map a file, both an active file handle and a
|
||||
* mapping handle is required, so is_open checks for a valid file handle, while
|
||||
* is_mapped checks for a valid mapping handle.
|
||||
* mapping handle is required, so `is_open` checks for a valid file handle, while
|
||||
* `is_mapped` checks for a valid mapping handle.
|
||||
*/
|
||||
bool is_open() const noexcept { return impl_.is_open(); }
|
||||
bool is_mapped() const noexcept { return impl_.is_mapped(); }
|
||||
bool empty() const noexcept { return impl_.empty(); }
|
||||
|
||||
/**
|
||||
* size/length returns the logical length (i.e. the one user requested), while
|
||||
* mapped_length returns the actual mapped length, which is usually a multiple of
|
||||
* the OS' page size.
|
||||
* `size` and `length` both return the logical length (i.e. the one user requested),
|
||||
* while `mapped_length` returns the actual mapped length, which is usually a
|
||||
* multiple of the underlying operating system's page allocation granularity.
|
||||
*/
|
||||
size_type size() const noexcept { return impl_.size(); }
|
||||
size_type length() const noexcept { return impl_.length(); }
|
||||
size_type mapped_length() const noexcept { return impl_.mapped_length(); }
|
||||
|
||||
/**
|
||||
* Returns a pointer to the first requested byte, or `nullptr` if no memory mapping
|
||||
* exists.
|
||||
*/
|
||||
pointer data() noexcept { return impl_.data(); }
|
||||
const_pointer data() const noexcept { return impl_.data(); }
|
||||
|
||||
/**
|
||||
* Returns an iterator to the first requested byte, if a valid memory mapping
|
||||
* exists, otherwise this function call is equivalent to invoking `end`.
|
||||
*/
|
||||
iterator begin() noexcept { return impl_.begin(); }
|
||||
const_iterator begin() const noexcept { return impl_.begin(); }
|
||||
const_iterator cbegin() const noexcept { return impl_.cbegin(); }
|
||||
|
||||
/** Returns an iterator one past the last requested byte. */
|
||||
iterator end() noexcept { return impl_.end(); }
|
||||
const_iterator end() const noexcept { return impl_.end(); }
|
||||
const_iterator cend() const noexcept { return impl_.cend(); }
|
||||
@ -202,21 +271,38 @@ public:
|
||||
const_reverse_iterator rend() const noexcept { return impl_.rend(); }
|
||||
const_reverse_iterator crend() const noexcept { return impl_.crend(); }
|
||||
|
||||
/**
|
||||
* Returns a reference to the `i`th byte from the first requested byte (as returned
|
||||
* by `data`). If this is invoked when no valid memory mapping has been established
|
||||
* prior to this call, undefined behaviour ensues.
|
||||
*/
|
||||
reference operator[](const size_type i) noexcept { return impl_[i]; }
|
||||
const_reference operator[](const size_type i) const noexcept { return impl_[i]; }
|
||||
|
||||
void map(const std::string& path, const size_type offset,
|
||||
const size_type length, std::error_code& error)
|
||||
{
|
||||
impl_.map(path, offset, length, impl_type::access_mode::read_only, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a read-only memory mapping.
|
||||
*
|
||||
* `handle` must be a valid file handle, which is then used to memory map the
|
||||
* requested region. Upon failure, `error` is set to indicate the reason and the
|
||||
* object remains in an unmapped state.
|
||||
*
|
||||
* When specifying `offset`, there is no need to worry about providing
|
||||
* a value that is aligned with the operating system's page allocation granularity.
|
||||
* This is adjusted by the implementation such that the first requested byte (as
|
||||
* returned by `data` or `begin`), so long as `offset` is valid, will be at `offset`
|
||||
* from the start of the file.
|
||||
*/
|
||||
void map(const handle_type handle, const size_type offset,
|
||||
const size_type length, std::error_code& error)
|
||||
{
|
||||
impl_.map(handle, offset, length, impl_type::access_mode::read_write, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a valid memory mapping has been established prior to this call, this call
|
||||
* instructs the kernel to unmap the memory region and dissasociate this object
|
||||
* from the file.
|
||||
*/
|
||||
void unmap() { impl_.unmap(); }
|
||||
|
||||
/** Flushes the memory mapped page to disk. */
|
||||
@ -235,6 +321,43 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Since `mmap_source` works on the file descriptor/handle level of abstraction, a
|
||||
* factory method is provided for the case when a file needs to be mapped using a file
|
||||
* path.
|
||||
*
|
||||
* Path may be `std::string`, `std::string_view`, `const char*`,
|
||||
* `std::filesystem::path`, `std::vector<char>`, or similar.
|
||||
*/
|
||||
template<typename Path>
|
||||
mmap_source make_mmap_source(const Path& path, mmap_source::size_type offset,
|
||||
mmap_source::size_type length, std::error_code& error)
|
||||
{
|
||||
const auto handle = detail::open_file(path,
|
||||
detail::mmap::access_mode::read_only, error);
|
||||
mmap_source mmap;
|
||||
if(!error) { mmap.map(handle, offset, length, error); }
|
||||
return mmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since `mmap_sink` works on the file descriptor/handle level of abstraction, a factory
|
||||
* method is provided for the case when a file needs to be mapped using a file path.
|
||||
*
|
||||
* Path may be `std::string`, `std::string_view`, `const char*`,
|
||||
* `std::filesystem::path`, `std::vector<char>`, or similar.
|
||||
*/
|
||||
template<typename Path>
|
||||
mmap_sink make_mmap_sink(const Path& path, mmap_sink::size_type offset,
|
||||
mmap_sink::size_type length, std::error_code& error)
|
||||
{
|
||||
const auto handle = detail::open_file(path,
|
||||
detail::mmap::access_mode::read_write, error);
|
||||
mmap_sink mmap;
|
||||
if(!error) { mmap.map(handle, offset, length, error); }
|
||||
return mmap;
|
||||
}
|
||||
|
||||
} // namespace mio
|
||||
|
||||
#endif // MIO_MMAP_HEADER
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
#include <cassert>
|
||||
#include <system_error>
|
||||
|
||||
int main(int argc, char** argv)
|
||||
int main()
|
||||
{
|
||||
const char* test_file_name = argc >= 2 ? argv[1] : "test-file";
|
||||
const char* test_file_name = "test-file";
|
||||
|
||||
std::string buffer(0x4000 - 250, 'M');
|
||||
|
||||
@ -17,8 +17,7 @@ int main(int argc, char** argv)
|
||||
file.close();
|
||||
|
||||
std::error_code error;
|
||||
mio::mmap_source file_view;
|
||||
file_view.map(test_file_name, 0, buffer.size(), error);
|
||||
mio::mmap_source file_view = mio::make_mmap_source(test_file_name, 0, buffer.size(), error);
|
||||
if(error)
|
||||
{
|
||||
const auto& errmsg = error.message();
|
||||
@ -39,4 +38,10 @@ int main(int argc, char** argv)
|
||||
assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
// see if mapping an invalid file results in an error
|
||||
mio::make_mmap_source("garbage-that-hopefully-doesn't exist", 0, 0, error);
|
||||
assert(error);
|
||||
|
||||
std::printf("all tests passed!\n");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user