├── IndirectBuffer.cpp ├── IndirectBuffer.h ├── LICENSE.md ├── Makefile ├── README.md ├── catch.hpp └── main.cpp /IndirectBuffer.cpp: -------------------------------------------------------------------------------- 1 | #include "IndirectBuffer.h" 2 | 3 | #include 4 | 5 | #if defined(__EMSCRIPTEN__) 6 | #include 7 | #endif 8 | 9 | IndirectBuffer::IndirectBuffer() { 10 | #if defined(__EMSCRIPTEN__) 11 | static int nextHandle = 1; 12 | _handle = ++nextHandle; 13 | 14 | EM_ASM_ARGS({ 15 | (Module._IB_ || (Module._IB_ = {}))[$0] = new Uint8Array(); 16 | }, _handle); 17 | #endif 18 | } 19 | 20 | IndirectBuffer::IndirectBuffer(size_t count) : IndirectBuffer() { 21 | resize(count); 22 | } 23 | 24 | IndirectBuffer::IndirectBuffer(const std::string &text) : IndirectBuffer((const uint8_t *)text.data(), text.size()) { 25 | } 26 | 27 | IndirectBuffer::IndirectBuffer(const std::vector &bytes) : IndirectBuffer(bytes.data(), bytes.size()) { 28 | } 29 | 30 | IndirectBuffer::IndirectBuffer(const uint8_t *bytes, size_t count) : IndirectBuffer(count) { 31 | set(0, bytes, count); 32 | } 33 | 34 | IndirectBuffer::IndirectBuffer(IndirectBuffer &&buffer) noexcept : IndirectBuffer() { 35 | *this = std::move(buffer); 36 | } 37 | 38 | IndirectBuffer::~IndirectBuffer() { 39 | #if defined(__EMSCRIPTEN__) 40 | EM_ASM_ARGS({ 41 | delete Module._IB_[$0]; 42 | }, _handle); 43 | #endif 44 | } 45 | 46 | IndirectBuffer &IndirectBuffer::operator = (IndirectBuffer &&buffer) noexcept { 47 | #if defined(__EMSCRIPTEN__) 48 | std::swap(_handle, buffer._handle); 49 | std::swap(_size, buffer._size); 50 | #else 51 | _data.swap(buffer._data); 52 | #endif 53 | return *this; 54 | } 55 | 56 | IndirectBuffer IndirectBuffer::clone() const { 57 | IndirectBuffer result(size()); 58 | result.copyFrom(0, size(), *this, 0); 59 | return result; 60 | } 61 | 62 | uint8_t IndirectBuffer::get(size_t index) const { 63 | assert(index < size()); 64 | 65 | #if defined(__EMSCRIPTEN__) 66 | return EM_ASM_INT({ 67 | return Module._IB_[$0][$1]; 68 | }, _handle, index); 69 | #else 70 | return _data[index]; 71 | #endif 72 | } 73 | 74 | void IndirectBuffer::set(size_t index, uint8_t byte) { 75 | assert(index < size()); 76 | 77 | #if defined(__EMSCRIPTEN__) 78 | EM_ASM_ARGS({ 79 | Module._IB_[$0][$1] = $2; 80 | }, _handle, index, byte); 81 | #else 82 | _data[index] = byte; 83 | #endif 84 | } 85 | 86 | void IndirectBuffer::get(size_t index, uint8_t *bytes, size_t count) const { 87 | assert(index + count <= size()); 88 | 89 | if (!bytes || !count) { 90 | return; 91 | } 92 | 93 | #if defined(__EMSCRIPTEN__) 94 | EM_ASM_ARGS({ 95 | Module.HEAP8.set(Module._IB_[$0].subarray($1, $1 + $3), $2); 96 | }, _handle, index, bytes, count); 97 | #else 98 | memcpy(bytes, _data.data() + index, count); 99 | #endif 100 | } 101 | 102 | void IndirectBuffer::set(size_t index, const uint8_t *bytes, size_t count) { 103 | assert(index + count <= size()); 104 | 105 | if (!bytes || !count) { 106 | return; 107 | } 108 | 109 | #if defined(__EMSCRIPTEN__) 110 | EM_ASM_ARGS({ 111 | Module._IB_[$0].set(Module.HEAP8.subarray($2, $2 + $3), $1); 112 | }, _handle, index, bytes, count); 113 | #else 114 | memcpy(_data.data() + index, bytes, count); 115 | #endif 116 | } 117 | 118 | size_t IndirectBuffer::size() const { 119 | #if defined(__EMSCRIPTEN__) 120 | return _size; 121 | #else 122 | return _data.size(); 123 | #endif 124 | } 125 | 126 | void IndirectBuffer::resize(size_t count) { 127 | #if defined(__EMSCRIPTEN__) 128 | EM_ASM_ARGS({ 129 | var old = Module._IB_[$0]; 130 | (Module._IB_[$0] = new Uint8Array($1)).set(old.length < $1 ? old : old.subarray(0, $1)); 131 | }, _handle, count); 132 | _size = count; 133 | #else 134 | _data.resize(count); 135 | #endif 136 | } 137 | 138 | void IndirectBuffer::move(size_t newIndex, size_t oldIndex, size_t count) { 139 | assert(oldIndex + count <= size()); 140 | assert(newIndex + count <= size()); 141 | 142 | if (oldIndex == newIndex) { 143 | return; 144 | } 145 | 146 | #if defined(__EMSCRIPTEN__) 147 | EM_ASM_ARGS({ 148 | var array = Module._IB_[$0]; 149 | array.set(array.subarray($2, $2 + $3), $1); 150 | }, _handle, newIndex, oldIndex, count); 151 | #else 152 | memmove(_data.data() + newIndex, _data.data() + oldIndex, count); 153 | #endif 154 | } 155 | 156 | void IndirectBuffer::copyFrom(size_t toIndex, size_t count, const IndirectBuffer &buffer, size_t fromIndex) { 157 | assert(fromIndex + count <= buffer.size()); 158 | assert(toIndex + count <= size()); 159 | 160 | if (this == &buffer) { 161 | move(toIndex, fromIndex, count); 162 | } else { 163 | #if defined(__EMSCRIPTEN__) 164 | EM_ASM_ARGS({ 165 | var fromArray = Module._IB_[$3]; 166 | var toArray = Module._IB_[$0]; 167 | toArray.set(fromArray.subarray($4, $4 + $2), $1); 168 | }, _handle, toIndex, count, buffer._handle, fromIndex); 169 | #else 170 | set(toIndex, buffer._data.data() + fromIndex, count); 171 | #endif 172 | } 173 | } 174 | 175 | IndirectBuffer IndirectBuffer::concat(const std::vector &buffers) { 176 | size_t totalSize = 0; 177 | for (const auto &buffer : buffers) { 178 | totalSize += buffer->size(); 179 | } 180 | IndirectBuffer result(totalSize); 181 | size_t index = 0; 182 | for (const auto &buffer : buffers) { 183 | result.copyFrom(index, buffer->size(), *buffer, 0); 184 | index += buffer->size(); 185 | } 186 | return std::move(result); 187 | } 188 | 189 | std::string IndirectBuffer::toString() const { 190 | std::string result; 191 | result.resize(size()); 192 | get(0, (uint8_t *)&result[0], size()); 193 | return result; 194 | } 195 | 196 | int IndirectBuffer::handleForEmscripten() const { 197 | #if defined(__EMSCRIPTEN__) 198 | return _handle; 199 | #else 200 | return 0; 201 | #endif 202 | } 203 | -------------------------------------------------------------------------------- /IndirectBuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // An indirect buffer is memoery that isn't stored in the emscripten heap when 7 | // compiled to the browser. This lets us get around the limitation of current 8 | // browsers (mostly Chrome) where there aren't enough continuous ranges of 9 | // address space to allocate large typed array objects. This is only an issue 10 | // in 32-bit browsers and is caused by various things including the use of 11 | // tcmalloc and ASLR (Address-Space Layout Randomization). This is just a 12 | // normal chunk of memory on other platforms. 13 | struct IndirectBuffer { 14 | // Constructors (use "explicit" to avoid cast-like behavior) 15 | IndirectBuffer(); 16 | explicit IndirectBuffer(size_t count); 17 | explicit IndirectBuffer(const std::string &text); 18 | explicit IndirectBuffer(const std::vector &bytes); 19 | IndirectBuffer(const uint8_t *bytes, size_t count); 20 | IndirectBuffer(IndirectBuffer &&buffer) noexcept; 21 | ~IndirectBuffer(); 22 | 23 | // Forbid copying because it's expensive and implicit, use clone() instead 24 | IndirectBuffer(const IndirectBuffer &) = delete; 25 | IndirectBuffer &operator = (const IndirectBuffer &) = delete; 26 | 27 | // Moving and cloning 28 | IndirectBuffer &operator = (IndirectBuffer &&buffer) noexcept; 29 | IndirectBuffer clone() const; 30 | 31 | // Element accessors 32 | uint8_t get(size_t index) const; 33 | void set(size_t index, uint8_t byte); 34 | 35 | // Range accessors 36 | void get(size_t index, uint8_t *bytes, size_t count) const; 37 | void set(size_t index, const uint8_t *bytes, size_t count); 38 | 39 | // Size-related functions 40 | bool empty() const { return size() == 0; } 41 | size_t size() const; 42 | void resize(size_t count); 43 | 44 | // Range updates that avoid touching the emscripten heap 45 | void move(size_t newIndex, size_t oldIndex, size_t count); 46 | void copyFrom(size_t toIndex, size_t count, const IndirectBuffer &buffer, size_t fromIndex); 47 | static IndirectBuffer concat(const std::vector &buffers); 48 | 49 | // Convenience function to copy everything into the heap 50 | std::string toString() const; 51 | 52 | // Pass this to JavaScript to get the Uint8Array from Module._IB_[handleForEmscripten] 53 | int handleForEmscripten() const; 54 | 55 | private: 56 | #if defined(__EMSCRIPTEN__) 57 | int _handle = 0; 58 | size_t _size = 0; 59 | #else 60 | std::vector _data; 61 | #endif 62 | }; 63 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: test-cpp test-js 2 | 3 | test-cpp: 4 | c++ main.cpp IndirectBuffer.cpp -std=c++11 5 | ./a.out 6 | rm a.out 7 | 8 | test-js: 9 | emcc main.cpp IndirectBuffer.cpp -std=c++11 10 | node a.out.js 11 | rm a.out.js 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IndirectBuffer 2 | 3 | This library provides a way for [emscripten](http://emscripten.org)-compiled C++ code to store large amounts of data outside of the main heap. Moving large allocations out of the main heap reduces memory fragmentation issues for long-running sessions, allows your code to use more of the limited address space in 32-bit browsers, and allows your code to break past the [31-bit typed array size limitation](https://github.com/WebKit/webkit/blob/f01d2bb66fcde2c3519c4f0c61f790387fd5faee/Source/JavaScriptCore/runtime/ArrayBuffer.h#L255) in 64-bit browsers. 4 | 5 | This library isn't exactly what we use internally at [Figma](https://www.figma.com) but it's the same general idea. We're providing this code because we think indirect memory storage is a big win and it's not described in the emscripten documentation. The closest analogy is the emscripten file system API but this IndirectBuffer API is simpler and easier to work with both from C++ and from JavaScript. 6 | 7 | To use it, just copy IndirectBuffer.h and IndirectBuffer.cpp into your project. The main.cpp and catch.hpp files included here are for unit testing (run "make test" assuming you have emscripten installed and activated). 8 | 9 | Example usage (out-of-heap image decode): 10 | 11 | ```cpp 12 | // Compile using: emcc example.cpp IndirectBuffer.cpp -std=c++11 -O3 -s NO_EXIT_RUNTIME=1 -s EXPORTED_FUNCTIONS="['_main','_DecodeImage_resize','_DecodeImage_finish']" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include "IndirectBuffer.h" 19 | 20 | struct DecodeImage { 21 | using Callback = std::function; 22 | 23 | DecodeImage(const std::string &url, const Callback &callback) : _callback(callback) { 24 | EM_ASM_ARGS({ 25 | var image = new Image; 26 | image.onload = function() { 27 | var width = image.width; 28 | var height = image.height; 29 | var canvas = document.createElement('canvas'); 30 | canvas.width = width; 31 | canvas.height = height; 32 | var context = canvas.getContext('2d'); 33 | context.drawImage(image, 0, 0); 34 | var pixels = context.getImageData(0, 0, width, height); 35 | var handle = Module._DecodeImage_resize($0, pixels.data.length); 36 | Module._IB_[handle].set(new Uint8Array(pixels.data.buffer)); 37 | Module._DecodeImage_finish($0, width, height); 38 | }; 39 | image.src = Module.Pointer_stringify($1); 40 | }, this, url.c_str()); 41 | } 42 | 43 | int resize(size_t size) { 44 | _buffer.resize(size); 45 | return _buffer.handleForEmscripten(); 46 | } 47 | 48 | void finish(int width, int height) { 49 | _callback(width, height, std::move(_buffer)); 50 | } 51 | 52 | private: 53 | Callback _callback; 54 | IndirectBuffer _buffer; 55 | }; 56 | 57 | extern "C" int DecodeImage_resize(DecodeImage *self, size_t size) { 58 | return self->resize(size); 59 | } 60 | 61 | extern "C" void DecodeImage_finish(DecodeImage *self, int width, int height) { 62 | self->finish(width, height); 63 | } 64 | 65 | static bool isImageOpaque(const IndirectBuffer &buffer) { 66 | return EM_ASM_INT({ 67 | var array = Module._IB_[$0]; 68 | for (var i = 3, n = array.length; i < n; i += 4) { 69 | if (array[i] < 255) { 70 | return false; 71 | } 72 | } 73 | return true; 74 | }, buffer.handleForEmscripten()); 75 | } 76 | 77 | int main() { 78 | static DecodeImage async("image.png", [](int width, int height, IndirectBuffer buffer) { 79 | printf("loaded %dx%d image outside main heap\n", width, height); 80 | printf("image is opaque: %d\n", isImageOpaque(buffer)); 81 | }); 82 | return 0; 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | 3 | #include "catch.hpp" 4 | #include "IndirectBuffer.h" 5 | 6 | TEST_CASE("IndirectBuffer size constructor") { 7 | IndirectBuffer buffer(2); 8 | REQUIRE(buffer.size() == 2); 9 | REQUIRE(buffer.get(0) == 0); 10 | REQUIRE(buffer.get(1) == 0); 11 | } 12 | 13 | TEST_CASE("IndirectBuffer data constructor") { 14 | uint8_t data[2] = { 123, 234 }; 15 | IndirectBuffer buffer(data, 2); 16 | REQUIRE(buffer.size() == 2); 17 | REQUIRE(buffer.get(0) == 123); 18 | REQUIRE(buffer.get(1) == 234); 19 | } 20 | 21 | TEST_CASE("IndirectBuffer get() and set()") { 22 | IndirectBuffer buffer; 23 | buffer.resize(8); 24 | 25 | buffer.set(3, 0x7F); 26 | buffer.set(4, 0x80); 27 | buffer.set(5, 0xFF); 28 | REQUIRE(buffer.get(0) == 0); 29 | REQUIRE(buffer.get(1) == 0); 30 | REQUIRE(buffer.get(2) == 0); 31 | REQUIRE(buffer.get(3) == 0x7F); 32 | REQUIRE(buffer.get(4) == 0x80); 33 | REQUIRE(buffer.get(5) == 0xFF); 34 | REQUIRE(buffer.get(6) == 0); 35 | REQUIRE(buffer.get(7) == 0); 36 | 37 | float value = M_PI; 38 | buffer.set(2, (uint8_t *)&value, sizeof(value)); 39 | REQUIRE(buffer.get(0) == 0); 40 | REQUIRE(buffer.get(1) == 0); 41 | REQUIRE(buffer.get(2) == ((uint8_t *)&value)[0]); 42 | REQUIRE(buffer.get(3) == ((uint8_t *)&value)[1]); 43 | REQUIRE(buffer.get(4) == ((uint8_t *)&value)[2]); 44 | REQUIRE(buffer.get(5) == ((uint8_t *)&value)[3]); 45 | REQUIRE(buffer.get(6) == 0); 46 | REQUIRE(buffer.get(7) == 0); 47 | 48 | value = 123; 49 | REQUIRE(value == 123); 50 | buffer.get(2, (uint8_t *)&value, sizeof(value)); 51 | REQUIRE(value == (float)M_PI); 52 | 53 | char data[8] = "testing"; 54 | buffer.set(0, (uint8_t *)data, sizeof(data)); 55 | REQUIRE(buffer.get(0) == 't'); 56 | REQUIRE(buffer.get(1) == 'e'); 57 | REQUIRE(buffer.get(2) == 's'); 58 | REQUIRE(buffer.get(3) == 't'); 59 | REQUIRE(buffer.get(4) == 'i'); 60 | REQUIRE(buffer.get(5) == 'n'); 61 | REQUIRE(buffer.get(6) == 'g'); 62 | REQUIRE(buffer.get(7) == '\0'); 63 | 64 | memset(data, -1, sizeof(data)); 65 | buffer.get(0, (uint8_t *)data, sizeof(data)); 66 | REQUIRE(data[0] == 't'); 67 | REQUIRE(data[1] == 'e'); 68 | REQUIRE(data[2] == 's'); 69 | REQUIRE(data[3] == 't'); 70 | REQUIRE(data[4] == 'i'); 71 | REQUIRE(data[5] == 'n'); 72 | REQUIRE(data[6] == 'g'); 73 | REQUIRE(data[7] == '\0'); 74 | } 75 | 76 | TEST_CASE("IndirectBuffer size() and resize()") { 77 | IndirectBuffer buffer; 78 | REQUIRE(buffer.size() == 0); 79 | 80 | buffer.resize(100); 81 | REQUIRE(buffer.size() == 100); 82 | 83 | buffer.resize(255 * 1024 * 1024); 84 | REQUIRE(buffer.size() == 255 * 1024 * 1024); 85 | 86 | buffer.resize(300); 87 | REQUIRE(buffer.size() == 300); 88 | 89 | buffer.resize(0); 90 | REQUIRE(buffer.size() == 0); 91 | 92 | buffer.resize(200); 93 | REQUIRE(buffer.size() == 200); 94 | } 95 | 96 | TEST_CASE("IndirectBuffer move constructor") { 97 | IndirectBuffer buffer1; 98 | buffer1.resize(2); 99 | buffer1.set(0, 123); 100 | buffer1.set(1, 234); 101 | IndirectBuffer buffer2(std::move(buffer1)); 102 | 103 | REQUIRE(buffer1.size() == 0); 104 | REQUIRE(buffer2.size() == 2); 105 | REQUIRE(buffer2.get(0) == 123); 106 | REQUIRE(buffer2.get(1) == 234); 107 | } 108 | 109 | TEST_CASE("IndirectBuffer move assignment operator") { 110 | IndirectBuffer buffer1; 111 | buffer1.resize(2); 112 | buffer1.set(0, 123); 113 | buffer1.set(1, 234); 114 | 115 | IndirectBuffer buffer2; 116 | buffer2.resize(3); 117 | buffer2.set(0, 1); 118 | buffer2.set(1, 2); 119 | buffer2.set(2, 3); 120 | 121 | buffer1 = std::move(buffer2); 122 | 123 | REQUIRE(buffer1.size() == 3); 124 | REQUIRE(buffer1.get(0) == 1); 125 | REQUIRE(buffer1.get(1) == 2); 126 | REQUIRE(buffer1.get(2) == 3); 127 | 128 | REQUIRE(buffer2.size() == 2); 129 | REQUIRE(buffer2.get(0) == 123); 130 | REQUIRE(buffer2.get(1) == 234); 131 | } 132 | 133 | TEST_CASE("IndirectBuffer clone()") { 134 | IndirectBuffer buffer1; 135 | buffer1.resize(2); 136 | buffer1.set(0, 123); 137 | buffer1.set(1, 234); 138 | 139 | IndirectBuffer buffer2 = buffer1.clone(); 140 | 141 | REQUIRE(buffer1.size() == 2); 142 | REQUIRE(buffer1.get(0) == 123); 143 | REQUIRE(buffer1.get(1) == 234); 144 | 145 | REQUIRE(buffer2.size() == 2); 146 | REQUIRE(buffer2.get(0) == 123); 147 | REQUIRE(buffer2.get(1) == 234); 148 | 149 | buffer1.set(0, 1); 150 | buffer1.set(1, 2); 151 | 152 | REQUIRE(buffer1.size() == 2); 153 | REQUIRE(buffer1.get(0) == 1); 154 | REQUIRE(buffer1.get(1) == 2); 155 | 156 | REQUIRE(buffer2.size() == 2); 157 | REQUIRE(buffer2.get(0) == 123); 158 | REQUIRE(buffer2.get(1) == 234); 159 | } 160 | 161 | TEST_CASE("IndirectBuffer move()") { 162 | IndirectBuffer buffer1(std::vector { 'a', 'b', 'c', 'd', 'e' }); 163 | buffer1.move(1, 2, 2); 164 | REQUIRE(buffer1.toString() == "acdde"); 165 | 166 | IndirectBuffer buffer2(std::vector { 'a', 'b', 'c', 'd', 'e' }); 167 | buffer2.move(2, 1, 2); 168 | REQUIRE(buffer2.toString() == "abbce"); 169 | } 170 | 171 | TEST_CASE("IndirectBuffer copyFrom()") { 172 | IndirectBuffer buffer1(std::vector { 'a', 'b', 'c', 'd', 'e' }); 173 | IndirectBuffer buffer2(std::vector { 'f', 'g', 'h', 'i', 'j', 'k' }); 174 | buffer2.copyFrom(3, 2, buffer1, 1); 175 | REQUIRE(buffer1.toString() == "abcde"); 176 | REQUIRE(buffer2.toString() == "fghbck"); 177 | } 178 | 179 | TEST_CASE("IndirectBuffer copyFrom() with self") { 180 | IndirectBuffer buffer1(std::vector { 'a', 'b', 'c', 'd', 'e' }); 181 | buffer1.copyFrom(1, 2, buffer1, 2); 182 | REQUIRE(buffer1.toString() == "acdde"); 183 | 184 | IndirectBuffer buffer2(std::vector { 'a', 'b', 'c', 'd', 'e' }); 185 | buffer2.copyFrom(2, 2, buffer2, 1); 186 | REQUIRE(buffer2.toString() == "abbce"); 187 | } 188 | 189 | TEST_CASE("IndirectBuffer concat()") { 190 | IndirectBuffer buffer1(std::vector { 'a', 'b', 'c' }); 191 | IndirectBuffer buffer2(std::vector { 'd', 'e' }); 192 | IndirectBuffer buffer3(std::vector {}); 193 | IndirectBuffer buffer4(std::vector { 'f' }); 194 | IndirectBuffer all = IndirectBuffer::concat(std::vector { &buffer1, &buffer2, &buffer3, &buffer4 }); 195 | REQUIRE(buffer1.toString() == "abc"); 196 | REQUIRE(buffer2.toString() == "de"); 197 | REQUIRE(buffer3.toString() == ""); 198 | REQUIRE(buffer4.toString() == "f"); 199 | REQUIRE(all.toString() == "abcdef"); 200 | } 201 | --------------------------------------------------------------------------------