Skip to content

Commit

Permalink
implement zero-copy buffers
Browse files Browse the repository at this point in the history
  • Loading branch information
mmomtchev committed Dec 25, 2023
1 parent 1123776 commit b0e78d9
Show file tree
Hide file tree
Showing 3 changed files with 12 additions and 27 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### [1.2.0]

- Support storing of a custom per-isolate data structure (`Napi::Env::GetInstanceData` and `Napi::Env::SetInstanceData`)
- Specify the memory ownership rules for `Buffer`
- Specify the memory ownership rules for `Buffer` and implement zero-copy transfers
- Fix a minor memory leak when calling method with incorrect number of arguments

### [1.1.1] 2023-12-01
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ NOBIND_MODULE(buffer, m) {

When C++ returns a `Buffer` object, that buffer is considered owned and it will be freed upon the destruction of the Node.js `Buffer` object by the garbage-collector.

When JavaScript passes a `Buffer` to a C++ method, data is copied - which is the safest but not the most efficient way to transfer it. Sharing the memory between C++ and JavaScript is possible in many cases - but must be carefully implemented in a custom typemap - especially when using async.
When JavaScript passes a `Buffer` to a C++ method, C++ receives a pointer to the underlying data region of the JS `Buffer` which is protected from the GC for duration of the call - including in async mode.

### Returning objects and factory functions

Expand Down
35 changes: 10 additions & 25 deletions include/nobuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,31 @@ namespace Nobind {

namespace Typemap {

// These typemaps transfer Buffers by copying the underlying data
// It is also possible to do it without copying if properly managing the GC
// but this requires very through understanding of all the underlying
// mechanisms by the user of nobind17

// In C++ a Node::Buffer decomposes to std::pair<uint8_t *, size_t>
using Buffer = std::pair<uint8_t *, size_t>;

// When calling C++ with a JS Buffer, a copy of the Buffer
// is created for the duration of the call
// This means that the typemap is completely safe for use with async
// code but it is not the most efficient way to transfer data
// When calling C++ with a JS Buffer, C++ receives a pointer
// to the Node.js Buffer region
// The JS Buffer object is protected from the GC for the
// duration of the call
template <> class FromJS<Buffer> {
Buffer val_;
Napi::ObjectReference persistent;

public:
inline explicit FromJS(Napi::Value val) {
if (!val.IsBuffer()) {
throw Napi::TypeError::New(val.Env(), "Expected a Buffer");
}
Napi::Buffer buf = val.As<Napi::Buffer<uint8_t>>();
val_ = {new uint8_t[buf.ByteLength()], buf.ByteLength()};
memcpy(val_.first, buf.Data() + buf.ByteOffset(), buf.ByteLength());
val_ = {buf.Data(), buf.ByteLength()};
persistent = Napi::Persistent(val.As<Napi::Object>());
}

inline Buffer Get() { return val_; }

inline ~FromJS() {
delete[] val_.first;
val_.first = nullptr;
val_.second = 0;
}

FromJS(const FromJS &) = delete;

// When moving, we must be sure that the old copy
// won't get freed
inline FromJS(FromJS &&rhs) {
val_ = rhs.val_;
rhs.val_.first = nullptr;
rhs.val_.second = 0;
};
inline FromJS(FromJS &&) = default;
};

// When receiving a Buffer from C++ we consider that
Expand All @@ -62,11 +45,13 @@ template <const ReturnAttribute &RETATTR> class ToJS<Buffer, RETATTR> {
inline Napi::Value Get() {
#ifdef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED
// Node-API does not support external buffers (Electron)
// C++ receives a copy with the original being freed immediately
Napi::Buffer buffer = Napi::Buffer<uint8_t>::Copy(env_, val_.first, val_.second);
delete[] val->data;
return buffer;
#else
// Node-API supports external buffers (Node.js)
// C++ receives ownership of the buffer which is freed upon collection of the JS Buffer object by the GC
return Napi::Buffer<uint8_t>::New(env_, val_.first, val_.second, [](Napi::Env, uint8_t *data) { delete[] data; });
#endif
}
Expand Down

0 comments on commit b0e78d9

Please sign in to comment.