Jacob Dufault
8 years ago
16 changed files with 630 additions and 145 deletions
@ -0,0 +1,2 @@ |
|||
#define DOCTEST_CONFIG_IMPLEMENT |
|||
#include "third_party/doctest/doctest/doctest.h" |
@ -1,92 +0,0 @@ |
|||
#ifdef _MSC_VER |
|||
#include "platform.h" |
|||
|
|||
#include <cassert> |
|||
#include <string> |
|||
#include <Windows.h> |
|||
|
|||
#include "utils.h" |
|||
|
|||
struct PlatformMutexWin : public PlatformMutex { |
|||
HANDLE raw_mutex = INVALID_HANDLE_VALUE; |
|||
|
|||
PlatformMutexWin(const std::string& name) { |
|||
raw_mutex = CreateMutex(nullptr, false /*initial_owner*/, name.c_str()); |
|||
assert(GetLastError() != ERROR_INVALID_HANDLE); |
|||
} |
|||
|
|||
~PlatformMutexWin() override { |
|||
ReleaseMutex(raw_mutex); |
|||
raw_mutex = INVALID_HANDLE_VALUE; |
|||
} |
|||
}; |
|||
|
|||
struct PlatformScopedMutexLockWin : public PlatformScopedMutexLock { |
|||
HANDLE raw_mutex; |
|||
|
|||
PlatformScopedMutexLockWin(HANDLE raw_mutex) : raw_mutex(raw_mutex) { |
|||
WaitForSingleObject(raw_mutex, INFINITE); |
|||
} |
|||
|
|||
~PlatformScopedMutexLockWin() override { |
|||
ReleaseMutex(raw_mutex); |
|||
} |
|||
}; |
|||
|
|||
struct PlatformSharedMemoryWin : public PlatformSharedMemory { |
|||
HANDLE shmem_; |
|||
|
|||
PlatformSharedMemoryWin(const std::string& name) { |
|||
this->name = name; |
|||
|
|||
shmem_ = CreateFileMapping( |
|||
INVALID_HANDLE_VALUE, |
|||
NULL, |
|||
PAGE_READWRITE, |
|||
0, |
|||
shmem_size, |
|||
name.c_str() |
|||
); |
|||
|
|||
shared = MapViewOfFile(shmem_, FILE_MAP_ALL_ACCESS, 0, 0, shmem_size); |
|||
} |
|||
|
|||
~PlatformSharedMemoryWin() override { |
|||
UnmapViewOfFile(shared); |
|||
shared = nullptr; |
|||
} |
|||
}; |
|||
|
|||
|
|||
|
|||
std::unique_ptr<PlatformMutex> CreatePlatformMutex(const std::string& name) { |
|||
return MakeUnique<PlatformMutexWin>(name); |
|||
} |
|||
|
|||
std::unique_ptr<PlatformScopedMutexLock> CreatePlatformScopedMutexLock(PlatformMutex* mutex) { |
|||
return MakeUnique<PlatformScopedMutexLockWin>(static_cast<PlatformMutexWin*>(mutex)->raw_mutex); |
|||
} |
|||
|
|||
std::unique_ptr<PlatformSharedMemory> CreatePlatformSharedMemory(const std::string& name) { |
|||
return MakeUnique<PlatformSharedMemoryWin>(name); |
|||
} |
|||
|
|||
// See http://stackoverflow.com/a/19535628
|
|||
std::string GetWorkingDirectory() { |
|||
char result[MAX_PATH]; |
|||
return std::string(result, GetModuleFileName(NULL, result, MAX_PATH)); |
|||
} |
|||
|
|||
/*
|
|||
// linux
|
|||
#include <string> |
|||
#include <limits.h> |
|||
#include <unistd.h> |
|||
|
|||
std::string getexepath() { |
|||
char result[ PATH_MAX ]; |
|||
ssize_t count = readlink( "/proc/self/exe", result, PATH_MAX ); |
|||
return std::string( result, (count > 0) ? count : 0 ); |
|||
} |
|||
*/ |
|||
#endif |
@ -0,0 +1,120 @@ |
|||
#include "buffer.h" |
|||
|
|||
#include <mutex> |
|||
|
|||
#include "platform.h" |
|||
#include "../utils.h" |
|||
#include "../third_party/doctest/doctest/doctest.h" |
|||
|
|||
namespace { |
|||
|
|||
struct ScopedLockLocal : public ScopedLock { |
|||
ScopedLockLocal(std::mutex& mutex) : guard(mutex) {} |
|||
std::lock_guard<std::mutex> guard; |
|||
}; |
|||
|
|||
struct BufferLocal : public Buffer { |
|||
explicit BufferLocal(size_t capacity) { |
|||
this->data = malloc(capacity); |
|||
this->capacity = capacity; |
|||
} |
|||
~BufferLocal() override { |
|||
free(data); |
|||
data = nullptr; |
|||
capacity = 0; |
|||
} |
|||
|
|||
std::unique_ptr<ScopedLock> WaitForExclusiveAccess() override { |
|||
return MakeUnique<ScopedLockLocal>(mutex_); |
|||
} |
|||
|
|||
std::mutex mutex_; |
|||
}; |
|||
|
|||
struct ScopedLockPlatform : public ScopedLock { |
|||
ScopedLockPlatform(PlatformMutex* mutex) |
|||
: guard(CreatePlatformScopedMutexLock(mutex)) {} |
|||
|
|||
std::unique_ptr<PlatformScopedMutexLock> guard; |
|||
}; |
|||
|
|||
struct BufferPlatform : public Buffer { |
|||
explicit BufferPlatform(const std::string& name, size_t capacity) |
|||
: memory_(CreatePlatformSharedMemory(name + "_mem", capacity)), |
|||
mutex_(CreatePlatformMutex(name + "_mtx")) { |
|||
this->data = memory_->data; |
|||
this->capacity = memory_->capacity; |
|||
} |
|||
|
|||
~BufferPlatform() override { |
|||
data = nullptr; |
|||
capacity = 0; |
|||
} |
|||
|
|||
std::unique_ptr<ScopedLock> WaitForExclusiveAccess() override { |
|||
return MakeUnique<ScopedLockPlatform>(mutex_.get()); |
|||
} |
|||
|
|||
std::unique_ptr<PlatformSharedMemory> memory_; |
|||
std::unique_ptr<PlatformMutex> mutex_; |
|||
}; |
|||
|
|||
} // namespace
|
|||
|
|||
std::unique_ptr<Buffer> Buffer::Create(size_t capacity) { |
|||
return MakeUnique<BufferLocal>(capacity); |
|||
} |
|||
|
|||
std::unique_ptr<Buffer> Buffer::CreateSharedBuffer(const std::string& name, size_t capacity) { |
|||
return MakeUnique<BufferPlatform>(name, capacity); |
|||
} |
|||
|
|||
TEST_SUITE("BufferLocal"); |
|||
|
|||
TEST_CASE("create") { |
|||
std::unique_ptr<Buffer> b = Buffer::Create(24); |
|||
REQUIRE(b->data); |
|||
REQUIRE(b->capacity == 24); |
|||
|
|||
b = Buffer::CreateSharedBuffer("indexertest", 24); |
|||
REQUIRE(b->data); |
|||
REQUIRE(b->capacity == 24); |
|||
} |
|||
|
|||
TEST_CASE("lock") { |
|||
auto buffers = { |
|||
Buffer::Create(sizeof(int)), |
|||
Buffer::CreateSharedBuffer("indexertest", sizeof(int)) |
|||
}; |
|||
|
|||
for (auto& b : buffers) { |
|||
int* data = reinterpret_cast<int*>(b->data); |
|||
*data = 0; |
|||
|
|||
std::unique_ptr<std::thread> thread; |
|||
{ |
|||
auto lock = b->WaitForExclusiveAccess(); |
|||
*data = 1; |
|||
|
|||
// Start a second thread, wait until it has attempted to acquire a lock.
|
|||
volatile bool did_read = false; |
|||
thread = MakeUnique<std::thread>([&did_read, &b, &data]() { |
|||
did_read = true; |
|||
auto l = b->WaitForExclusiveAccess(); |
|||
*data = 2; |
|||
}); |
|||
while (!did_read) |
|||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
|||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
|||
|
|||
// Verify lock acquisition is waiting.
|
|||
REQUIRE(*data == 1); |
|||
} |
|||
|
|||
// Wait for thread to acquire lock, verify it writes to data.
|
|||
thread->join(); |
|||
REQUIRE(*data == 2); |
|||
} |
|||
} |
|||
|
|||
TEST_SUITE_END(); |
@ -0,0 +1,30 @@ |
|||
#pragma once |
|||
|
|||
#include <memory> |
|||
#include <string> |
|||
|
|||
struct ScopedLock { |
|||
virtual ~ScopedLock() = default; |
|||
}; |
|||
|
|||
// Points to a generic block of memory. Note that |data| is relocatable, ie,
|
|||
// multiple Buffer instantations may point to the same underlying block of
|
|||
// memory but the data pointer has different values.
|
|||
struct Buffer { |
|||
// Create a new buffer of the given capacity using process-local memory.
|
|||
static std::unique_ptr<Buffer> Create(size_t capacity); |
|||
// Create a buffer pointing to memory shared across processes with the given
|
|||
// capacity.
|
|||
static std::unique_ptr<Buffer> CreateSharedBuffer(const std::string& name, |
|||
size_t capacity); |
|||
|
|||
virtual ~Buffer() = default; |
|||
|
|||
// Acquire a lock on the buffer, ie, become the only code that can read or
|
|||
// write to it. The lock lasts so long as the returned object is alive.
|
|||
virtual std::unique_ptr<ScopedLock> WaitForExclusiveAccess() = 0; |
|||
|
|||
void* data = nullptr; |
|||
size_t capacity = 0; |
|||
}; |
|||
|
@ -0,0 +1,37 @@ |
|||
#include "message_queue.h" |
|||
|
|||
#include <cassert> |
|||
|
|||
struct MessageQueue::BufferMetadata { |
|||
// Total number of used bytes exluding the sizeof this metadata object.
|
|||
void set_total_messages_byte_count(size_t used_bytes) { |
|||
total_message_bytes_ = used_bytes; |
|||
} |
|||
|
|||
// The total number of bytes in use.
|
|||
size_t total_bytes_used_including_metadata() { |
|||
return total_message_bytes_ + sizeof(BufferMetadata); |
|||
} |
|||
|
|||
// The total number of bytes currently used for messages. This does not
|
|||
// include the sizeof the buffer metadata.
|
|||
size_t total_message_bytes() { |
|||
return total_message_bytes_; |
|||
} |
|||
|
|||
private: |
|||
size_t total_message_bytes_ = 0; |
|||
}; |
|||
|
|||
MessageQueue::MessageQueue(std::unique_ptr<Buffer> buffer, bool buffer_has_data) : buffer_(std::move(buffer)) { |
|||
if (!buffer_has_data) |
|||
new(buffer_->data) BufferMetadata(); |
|||
} |
|||
|
|||
void MessageQueue::Enqueue(const Message& message) { |
|||
|
|||
} |
|||
|
|||
MessageQueue::BufferMetadata* MessageQueue::Metadata() { |
|||
return reinterpret_cast<BufferMetadata*>(buffer_->data); |
|||
} |
@ -0,0 +1,75 @@ |
|||
#pragma once |
|||
|
|||
#include <vector> |
|||
#include <memory> |
|||
|
|||
#include "buffer.h" |
|||
|
|||
struct Message { |
|||
// Unique message identifier.
|
|||
uint8_t message_id; |
|||
|
|||
// Total size of the message (including metadata that this object stores).
|
|||
size_t total_size; |
|||
}; |
|||
|
|||
// A MessageQueue is a FIFO container storing messages in an arbitrary memory
|
|||
// buffer.
|
|||
// - Multiple separate MessageQueues instantiations can point to the
|
|||
// same underlying buffer
|
|||
// - Buffer is fully relocatable, ie, it can have multiple different
|
|||
// addresses (as is the case for memory shared across processes).
|
|||
struct MessageQueue { |
|||
// Create a new MessageQueue using |buffer| as the backing data storage.
|
|||
// This does *not* take ownership over the memory stored in |buffer|.
|
|||
//
|
|||
// If |buffer_has_data| is true, then it is assumed that |buffer| contains
|
|||
// data and has already been initialized. It is a perfectly acceptable
|
|||
// use-case to have multiple completely separate MessageQueue
|
|||
// instantiations pointing to the same memory.
|
|||
explicit MessageQueue(std::unique_ptr<Buffer> buffer, bool buffer_has_data); |
|||
MessageQueue(const MessageQueue&) = delete; |
|||
|
|||
// Enqueue a message to the queue. This will wait until there is room in
|
|||
// queue. If the message is too large to fit into the queue, this will
|
|||
// wait until the message has been fully sent, which may involve multiple
|
|||
// IPC roundtrips (ie, Enqueue -> DequeueAll -> Enqueue) - so this method
|
|||
// may take a long time to run.
|
|||
//
|
|||
// TODO: Consider copying message memory to a temporary buffer and running
|
|||
// enqueues on a worker thread.
|
|||
void Enqueue(const Message& message); |
|||
|
|||
// Take all messages from the queue.
|
|||
//
|
|||
// note:
|
|||
// We could make this allocation free by returning raw pointers to the
|
|||
// internal process-local buffer, but that is pretty haphazard and likely
|
|||
// to cause a very confusing crash. The extra memory allocations here from
|
|||
// unique_ptr going to make a performance difference.
|
|||
std::vector<std::unique_ptr<Message>> DequeueAll(); |
|||
|
|||
// Take the first available message from the queue.
|
|||
std::unique_ptr<Message> DequeueFirst(); |
|||
|
|||
private: |
|||
struct BufferMetadata; |
|||
|
|||
BufferMetadata* Metadata(); |
|||
|
|||
std::unique_ptr<Buffer> buffer_; |
|||
}; |
|||
|
|||
|
|||
/*
|
|||
// TODO: We convert IpcMessage <-> Message as a user-level operation.
|
|||
// MessageQueue doesn't know about IpcMessage.
|
|||
struct IpcMessage { |
|||
std::unique_ptr<Message> ToMessage(); |
|||
void BuildFromMessage(std::unique_ptr<Message> message); |
|||
|
|||
// Serialize/deserialize the message.
|
|||
virtual void Serialize(Writer& writer) = 0; |
|||
virtual void Deserialize(Reader& reader) = 0; |
|||
}; |
|||
*/ |
@ -0,0 +1,39 @@ |
|||
#include "platform.h" |
|||
|
|||
#include <thread> |
|||
|
|||
#include "../third_party/doctest/doctest/doctest.h" |
|||
|
|||
|
|||
TEST_SUITE("Platform"); |
|||
|
|||
|
|||
TEST_CASE("Mutex lock/unlock (single process)") { |
|||
auto m1 = CreatePlatformMutex("indexer-platformmutexttest"); |
|||
auto l1 = CreatePlatformScopedMutexLock(m1.get()); |
|||
auto m2 = CreatePlatformMutex("indexer-platformmutexttest"); |
|||
|
|||
int value = 0; |
|||
|
|||
volatile bool did_run = false; |
|||
std::thread t([&]() { |
|||
did_run = true; |
|||
auto l2 = CreatePlatformScopedMutexLock(m2.get()); |
|||
value = 1; |
|||
}); |
|||
while (!did_run) |
|||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
|||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
|||
|
|||
// Other thread has had a chance to run, but it should not have
|
|||
// written to value yet (ie, it should be waiting).
|
|||
REQUIRE(value == 0); |
|||
|
|||
// Release the lock, wait for other thread to finish. Verify it
|
|||
// wrote the expected value.
|
|||
l1.reset(); |
|||
t.join(); |
|||
REQUIRE(value == 1); |
|||
} |
|||
|
|||
TEST_SUITE_END(); |
@ -0,0 +1,113 @@ |
|||
#if defined(_WIN32) |
|||
#include "platform.h" |
|||
|
|||
#include <cassert> |
|||
#include <iostream> |
|||
#include <string> |
|||
#include <Windows.h> |
|||
|
|||
#include "../utils.h" |
|||
|
|||
namespace { |
|||
|
|||
DWORD CheckForError(std::vector<DWORD> allow) { |
|||
DWORD error = GetLastError(); |
|||
if (error == ERROR_SUCCESS || std::find(allow.begin(), allow.end(), error) != allow.end()) |
|||
return error; |
|||
|
|||
// See http://stackoverflow.com/a/17387176
|
|||
LPSTR message_buffer = nullptr; |
|||
size_t size = FormatMessageA( |
|||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, |
|||
NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&message_buffer, 0, NULL); |
|||
std::string message(message_buffer, size); |
|||
LocalFree(message_buffer); |
|||
|
|||
std::cerr << "Windows error code=" << error << ", message=" << message << std::endl; |
|||
assert(false); // debugger break
|
|||
exit(1); |
|||
} |
|||
|
|||
struct PlatformMutexWin : public PlatformMutex { |
|||
HANDLE raw_mutex = INVALID_HANDLE_VALUE; |
|||
|
|||
PlatformMutexWin(const std::string& name) { |
|||
std::cerr << "[win] Creating mutex with name " << name << std::endl; |
|||
raw_mutex = CreateMutex(nullptr, false /*initial_owner*/, name.c_str()); |
|||
CheckForError({ ERROR_ALREADY_EXISTS }); |
|||
} |
|||
|
|||
~PlatformMutexWin() override { |
|||
CloseHandle(raw_mutex); |
|||
CheckForError({} /*allow*/); |
|||
raw_mutex = INVALID_HANDLE_VALUE; |
|||
} |
|||
}; |
|||
|
|||
struct PlatformScopedMutexLockWin : public PlatformScopedMutexLock { |
|||
HANDLE raw_mutex; |
|||
|
|||
PlatformScopedMutexLockWin(HANDLE raw_mutex) : raw_mutex(raw_mutex) { |
|||
DWORD result = WaitForSingleObject(raw_mutex, INFINITE); |
|||
assert(result != WAIT_FAILED); |
|||
CheckForError({} /*allow*/); |
|||
} |
|||
|
|||
~PlatformScopedMutexLockWin() override { |
|||
ReleaseMutex(raw_mutex); |
|||
CheckForError({} /*allow*/); |
|||
} |
|||
}; |
|||
|
|||
struct PlatformSharedMemoryWin : public PlatformSharedMemory { |
|||
HANDLE shmem_; |
|||
|
|||
PlatformSharedMemoryWin(const std::string& name, size_t capacity) { |
|||
std::cerr << "[win] Creating shared memory with name " << name << " and capacity " << capacity << std::endl; |
|||
this->name = name; |
|||
|
|||
shmem_ = CreateFileMapping( |
|||
INVALID_HANDLE_VALUE, |
|||
NULL, |
|||
PAGE_READWRITE, |
|||
0, |
|||
capacity, |
|||
name.c_str() |
|||
); |
|||
CheckForError({ ERROR_ALREADY_EXISTS } /*allow*/); |
|||
|
|||
data = MapViewOfFile(shmem_, FILE_MAP_ALL_ACCESS, 0, 0, capacity); |
|||
CheckForError({ ERROR_ALREADY_EXISTS } /*allow*/); |
|||
|
|||
this->capacity = capacity; |
|||
} |
|||
|
|||
~PlatformSharedMemoryWin() override { |
|||
UnmapViewOfFile(data); |
|||
CheckForError({} /*allow*/); |
|||
|
|||
data = nullptr; |
|||
capacity = 0; |
|||
} |
|||
}; |
|||
|
|||
} // namespace
|
|||
|
|||
std::unique_ptr<PlatformMutex> CreatePlatformMutex(const std::string& name) { |
|||
return MakeUnique<PlatformMutexWin>(name); |
|||
} |
|||
|
|||
std::unique_ptr<PlatformScopedMutexLock> CreatePlatformScopedMutexLock(PlatformMutex* mutex) { |
|||
return MakeUnique<PlatformScopedMutexLockWin>(static_cast<PlatformMutexWin*>(mutex)->raw_mutex); |
|||
} |
|||
|
|||
std::unique_ptr<PlatformSharedMemory> CreatePlatformSharedMemory(const std::string& name, size_t size) { |
|||
return MakeUnique<PlatformSharedMemoryWin>(name, size); |
|||
} |
|||
|
|||
// See http://stackoverflow.com/a/19535628
|
|||
std::string GetWorkingDirectory() { |
|||
char result[MAX_PATH]; |
|||
return std::string(result, GetModuleFileName(NULL, result, MAX_PATH)); |
|||
} |
|||
#endif |
@ -0,0 +1,100 @@ |
|||
#include "resizable_buffer.h" |
|||
|
|||
#include "../third_party/doctest/doctest/doctest.h" |
|||
|
|||
#include <cassert> |
|||
#include <cstdint> |
|||
#include <cstdlib> |
|||
#include <cstring> |
|||
|
|||
namespace { |
|||
const size_t kInitialCapacity = 128; |
|||
} |
|||
|
|||
ResizableBuffer::ResizableBuffer() { |
|||
buffer = malloc(kInitialCapacity); |
|||
size = 0; |
|||
capacity_ = kInitialCapacity; |
|||
} |
|||
|
|||
ResizableBuffer::~ResizableBuffer() { |
|||
free(buffer); |
|||
size = 0; |
|||
capacity_ = 0; |
|||
} |
|||
|
|||
void ResizableBuffer::Append(void* content, size_t content_size) { |
|||
assert(capacity_ >= 0); |
|||
|
|||
size_t new_size = size + content_size; |
|||
|
|||
// Grow buffer capacity if needed.
|
|||
if (new_size >= capacity_) { |
|||
size_t new_capacity = capacity_ * 2; |
|||
while (new_size >= new_capacity) |
|||
new_capacity *= 2; |
|||
void* new_memory = malloc(new_capacity); |
|||
assert(size < capacity_); |
|||
memcpy(new_memory, buffer, size); |
|||
free(buffer); |
|||
buffer = new_memory; |
|||
capacity_ = new_capacity; |
|||
} |
|||
|
|||
// Append new content into memory.
|
|||
memcpy(reinterpret_cast<uint8_t*>(buffer) + size, content, content_size); |
|||
size = new_size; |
|||
} |
|||
|
|||
void ResizableBuffer::Reset() { |
|||
size = 0; |
|||
} |
|||
|
|||
TEST_SUITE("ResizableBuffer"); |
|||
|
|||
TEST_CASE("buffer starts with zero size") { |
|||
ResizableBuffer b; |
|||
REQUIRE(b.buffer); |
|||
REQUIRE(b.size == 0); |
|||
} |
|||
|
|||
TEST_CASE("append and reset") { |
|||
int content = 1; |
|||
ResizableBuffer b; |
|||
|
|||
b.Append(&content, sizeof(content)); |
|||
REQUIRE(b.size == sizeof(content)); |
|||
|
|||
b.Append(&content, sizeof(content)); |
|||
REQUIRE(b.size == (2 * sizeof(content))); |
|||
|
|||
b.Reset(); |
|||
REQUIRE(b.size == 0); |
|||
} |
|||
|
|||
TEST_CASE("appended content is copied into buffer w/ resize") { |
|||
int content = 0; |
|||
ResizableBuffer b; |
|||
|
|||
// go past kInitialCapacity to verify resize works too
|
|||
while (b.size < kInitialCapacity * 2) { |
|||
b.Append(&content, sizeof(content)); |
|||
content += 1; |
|||
} |
|||
|
|||
for (int i = 0; i < content; ++i) |
|||
REQUIRE(i == *(reinterpret_cast<int*>(b.buffer) + i)); |
|||
} |
|||
|
|||
TEST_CASE("reset does not reallocate") { |
|||
ResizableBuffer b; |
|||
|
|||
while (b.size < kInitialCapacity) |
|||
b.Append(&b, sizeof(b)); |
|||
|
|||
void* buffer = b.buffer; |
|||
b.Reset(); |
|||
REQUIRE(b.buffer == buffer); |
|||
} |
|||
|
|||
TEST_SUITE_END(); |
@ -0,0 +1,21 @@ |
|||
#pragma once |
|||
|
|||
// Points to a generic block of memory that can be resized. This class owns
|
|||
// and has the only pointer to the underlying memory buffer.
|
|||
struct ResizableBuffer { |
|||
ResizableBuffer(); |
|||
ResizableBuffer(const ResizableBuffer&) = delete; |
|||
~ResizableBuffer(); |
|||
|
|||
void Append(void* content, size_t content_size); |
|||
void Reset(); |
|||
|
|||
// Buffer content.
|
|||
void* buffer; |
|||
// Number of bytes in |buffer|. Note that the actual buffer may be larger
|
|||
// than |size|.
|
|||
size_t size; |
|||
|
|||
private: |
|||
size_t capacity_; |
|||
}; |
Loading…
Reference in new issue