@camstack/shm-ring 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/binding.gyp +35 -0
- package/dist/frame-ring-reader-cache.d.ts +22 -0
- package/dist/frame-ring-reader-cache.d.ts.map +1 -0
- package/dist/frame-ring.d.ts +379 -0
- package/dist/frame-ring.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +655 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +643 -0
- package/dist/index.mjs.map +1 -0
- package/dist/native.d.ts +34 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +226 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +224 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/testing/looping-clip-frame-source.d.ts +57 -0
- package/dist/testing/looping-clip-frame-source.d.ts.map +1 -0
- package/package.json +59 -0
- package/prebuilds/linux-x64/@camstack+shm-ring.node +0 -0
- package/src/shm.cc +321 -0
package/src/shm.cc
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shm_ring — CamStack shared-memory segment N-API addon.
|
|
3
|
+
*
|
|
4
|
+
* Maps/unmaps a named OS shared-memory segment and hands JS a zero-copy
|
|
5
|
+
* Buffer over the mapping. No ring/seqlock logic here (that is pure TS,
|
|
6
|
+
* Task 2). One #ifdef _WIN32 split:
|
|
7
|
+
* - POSIX (macOS/Linux): shm_open + ftruncate + mmap / munmap / shm_unlink.
|
|
8
|
+
* - Windows: CreateFileMappingW / OpenFileMappingW + MapViewOfFile,
|
|
9
|
+
* UnmapViewOfFile + CloseHandle; the OS reclaims the segment when the
|
|
10
|
+
* last handle closes, so unlink() is a documented no-op.
|
|
11
|
+
*
|
|
12
|
+
* Each create()/open() returns { buffer, handle } where `handle` is an
|
|
13
|
+
* Napi::External wrapping a heap-allocated ShmHandle. close(handle) reaches
|
|
14
|
+
* the native ShmHandle to unmap. The Buffer finalizer does NOT free the
|
|
15
|
+
* mapping — close() owns that lifecycle.
|
|
16
|
+
*/
|
|
17
|
+
#include <napi.h>
|
|
18
|
+
|
|
19
|
+
#include <cstdint>
|
|
20
|
+
#include <string>
|
|
21
|
+
|
|
22
|
+
#ifdef _WIN32
|
|
23
|
+
#include <windows.h>
|
|
24
|
+
#else
|
|
25
|
+
#include <fcntl.h>
|
|
26
|
+
#include <sys/mman.h>
|
|
27
|
+
#include <sys/stat.h>
|
|
28
|
+
#include <unistd.h>
|
|
29
|
+
#include <cerrno>
|
|
30
|
+
#include <cstring>
|
|
31
|
+
#endif
|
|
32
|
+
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
// Native-side record for a single mapped segment. Heap-allocated; its
|
|
36
|
+
// lifetime is owned by the JS-side Napi::External handle.
|
|
37
|
+
struct ShmHandle {
|
|
38
|
+
void* ptr = nullptr;
|
|
39
|
+
size_t byteLength = 0;
|
|
40
|
+
#ifdef _WIN32
|
|
41
|
+
HANDLE mapping = nullptr;
|
|
42
|
+
#else
|
|
43
|
+
int fd = -1;
|
|
44
|
+
#endif
|
|
45
|
+
bool unmapped = false;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
#ifdef _WIN32
|
|
49
|
+
|
|
50
|
+
// Translate a UTF-8 segment name into a wide string for the Win32 W APIs.
|
|
51
|
+
std::wstring Widen(const std::string& utf8) {
|
|
52
|
+
if (utf8.empty()) return std::wstring();
|
|
53
|
+
int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(),
|
|
54
|
+
static_cast<int>(utf8.size()), nullptr, 0);
|
|
55
|
+
std::wstring wide(static_cast<size_t>(len), L'\0');
|
|
56
|
+
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(),
|
|
57
|
+
static_cast<int>(utf8.size()), &wide[0], len);
|
|
58
|
+
return wide;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#endif
|
|
62
|
+
|
|
63
|
+
// Unmap a segment's view of the mapping. Idempotent.
|
|
64
|
+
void UnmapHandle(ShmHandle* handle) {
|
|
65
|
+
if (handle == nullptr || handle->unmapped) return;
|
|
66
|
+
handle->unmapped = true;
|
|
67
|
+
#ifdef _WIN32
|
|
68
|
+
if (handle->ptr != nullptr) {
|
|
69
|
+
UnmapViewOfFile(handle->ptr);
|
|
70
|
+
}
|
|
71
|
+
if (handle->mapping != nullptr) {
|
|
72
|
+
CloseHandle(handle->mapping);
|
|
73
|
+
handle->mapping = nullptr;
|
|
74
|
+
}
|
|
75
|
+
#else
|
|
76
|
+
if (handle->ptr != nullptr && handle->ptr != MAP_FAILED) {
|
|
77
|
+
munmap(handle->ptr, handle->byteLength);
|
|
78
|
+
}
|
|
79
|
+
if (handle->fd >= 0) {
|
|
80
|
+
::close(handle->fd);
|
|
81
|
+
handle->fd = -1;
|
|
82
|
+
}
|
|
83
|
+
#endif
|
|
84
|
+
handle->ptr = nullptr;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Finalizer for the Napi::External wrapping a ShmHandle. If JS dropped the
|
|
88
|
+
// handle without an explicit close(), unmap here as a safety net, then free.
|
|
89
|
+
void FinalizeHandle(Napi::Env /*env*/, ShmHandle* handle) {
|
|
90
|
+
if (handle == nullptr) return;
|
|
91
|
+
UnmapHandle(handle);
|
|
92
|
+
delete handle;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build the { buffer, handle } result object. The Buffer finalizer is a
|
|
96
|
+
// no-op — close()/the External finalizer own the unmap.
|
|
97
|
+
Napi::Object MakeSegment(Napi::Env env, ShmHandle* handle) {
|
|
98
|
+
Napi::Buffer<uint8_t> buffer = Napi::Buffer<uint8_t>::New(
|
|
99
|
+
env, static_cast<uint8_t*>(handle->ptr), handle->byteLength,
|
|
100
|
+
[](Napi::Env, uint8_t*) { /* mapping freed by close()/External */ });
|
|
101
|
+
|
|
102
|
+
Napi::External<ShmHandle> external =
|
|
103
|
+
Napi::External<ShmHandle>::New(env, handle, FinalizeHandle);
|
|
104
|
+
|
|
105
|
+
Napi::Object result = Napi::Object::New(env);
|
|
106
|
+
result.Set("buffer", buffer);
|
|
107
|
+
result.Set("handle", external);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// create(name: string, byteLength: number) -> { buffer, handle }
|
|
112
|
+
Napi::Value Create(const Napi::CallbackInfo& info) {
|
|
113
|
+
Napi::Env env = info.Env();
|
|
114
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsNumber()) {
|
|
115
|
+
Napi::TypeError::New(env, "create(name: string, byteLength: number)")
|
|
116
|
+
.ThrowAsJavaScriptException();
|
|
117
|
+
return env.Undefined();
|
|
118
|
+
}
|
|
119
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
120
|
+
int64_t requested = info[1].As<Napi::Number>().Int64Value();
|
|
121
|
+
if (requested <= 0) {
|
|
122
|
+
Napi::RangeError::New(env, "byteLength must be > 0")
|
|
123
|
+
.ThrowAsJavaScriptException();
|
|
124
|
+
return env.Undefined();
|
|
125
|
+
}
|
|
126
|
+
size_t byteLength = static_cast<size_t>(requested);
|
|
127
|
+
|
|
128
|
+
#ifdef _WIN32
|
|
129
|
+
std::wstring wname = Widen(name);
|
|
130
|
+
DWORD hi = static_cast<DWORD>((static_cast<uint64_t>(byteLength) >> 32) & 0xffffffff);
|
|
131
|
+
DWORD lo = static_cast<DWORD>(static_cast<uint64_t>(byteLength) & 0xffffffff);
|
|
132
|
+
HANDLE mapping = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr,
|
|
133
|
+
PAGE_READWRITE, hi, lo, wname.c_str());
|
|
134
|
+
if (mapping == nullptr) {
|
|
135
|
+
std::string msg = "CreateFileMappingW failed: GetLastError=";
|
|
136
|
+
msg += std::to_string(GetLastError());
|
|
137
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
138
|
+
return env.Undefined();
|
|
139
|
+
}
|
|
140
|
+
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
|
141
|
+
CloseHandle(mapping);
|
|
142
|
+
Napi::Error::New(env, "shared-memory segment already exists")
|
|
143
|
+
.ThrowAsJavaScriptException();
|
|
144
|
+
return env.Undefined();
|
|
145
|
+
}
|
|
146
|
+
void* ptr = MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, byteLength);
|
|
147
|
+
if (ptr == nullptr) {
|
|
148
|
+
std::string msg = "MapViewOfFile failed: GetLastError=";
|
|
149
|
+
msg += std::to_string(GetLastError());
|
|
150
|
+
CloseHandle(mapping);
|
|
151
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
152
|
+
return env.Undefined();
|
|
153
|
+
}
|
|
154
|
+
ShmHandle* handle = new ShmHandle();
|
|
155
|
+
handle->ptr = ptr;
|
|
156
|
+
handle->byteLength = byteLength;
|
|
157
|
+
handle->mapping = mapping;
|
|
158
|
+
#else
|
|
159
|
+
int fd = shm_open(name.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600);
|
|
160
|
+
if (fd < 0) {
|
|
161
|
+
std::string msg = "shm_open(create) failed: ";
|
|
162
|
+
msg += std::strerror(errno);
|
|
163
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
164
|
+
return env.Undefined();
|
|
165
|
+
}
|
|
166
|
+
if (ftruncate(fd, static_cast<off_t>(byteLength)) != 0) {
|
|
167
|
+
std::string msg = "ftruncate failed: ";
|
|
168
|
+
msg += std::strerror(errno);
|
|
169
|
+
::close(fd);
|
|
170
|
+
shm_unlink(name.c_str());
|
|
171
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
172
|
+
return env.Undefined();
|
|
173
|
+
}
|
|
174
|
+
void* ptr = mmap(nullptr, byteLength, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
175
|
+
if (ptr == MAP_FAILED) {
|
|
176
|
+
std::string msg = "mmap failed: ";
|
|
177
|
+
msg += std::strerror(errno);
|
|
178
|
+
::close(fd);
|
|
179
|
+
shm_unlink(name.c_str());
|
|
180
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
181
|
+
return env.Undefined();
|
|
182
|
+
}
|
|
183
|
+
ShmHandle* handle = new ShmHandle();
|
|
184
|
+
handle->ptr = ptr;
|
|
185
|
+
handle->byteLength = byteLength;
|
|
186
|
+
handle->fd = fd;
|
|
187
|
+
#endif
|
|
188
|
+
|
|
189
|
+
return MakeSegment(env, handle);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// open(name: string, byteLength: number) -> { buffer, handle }
|
|
193
|
+
Napi::Value Open(const Napi::CallbackInfo& info) {
|
|
194
|
+
Napi::Env env = info.Env();
|
|
195
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsNumber()) {
|
|
196
|
+
Napi::TypeError::New(env, "open(name: string, byteLength: number)")
|
|
197
|
+
.ThrowAsJavaScriptException();
|
|
198
|
+
return env.Undefined();
|
|
199
|
+
}
|
|
200
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
201
|
+
int64_t requested = info[1].As<Napi::Number>().Int64Value();
|
|
202
|
+
if (requested <= 0) {
|
|
203
|
+
Napi::RangeError::New(env, "byteLength must be > 0")
|
|
204
|
+
.ThrowAsJavaScriptException();
|
|
205
|
+
return env.Undefined();
|
|
206
|
+
}
|
|
207
|
+
size_t byteLength = static_cast<size_t>(requested);
|
|
208
|
+
|
|
209
|
+
#ifdef _WIN32
|
|
210
|
+
std::wstring wname = Widen(name);
|
|
211
|
+
HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
|
|
212
|
+
if (mapping == nullptr) {
|
|
213
|
+
std::string msg = "OpenFileMappingW failed: segment not found: GetLastError=";
|
|
214
|
+
msg += std::to_string(GetLastError());
|
|
215
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
216
|
+
return env.Undefined();
|
|
217
|
+
}
|
|
218
|
+
// MapViewOfFile rejects a view larger than the segment with
|
|
219
|
+
// ERROR_ACCESS_DENIED — it fails cleanly, no SIGBUS risk like POSIX.
|
|
220
|
+
void* ptr = MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, byteLength);
|
|
221
|
+
if (ptr == nullptr) {
|
|
222
|
+
std::string msg = "MapViewOfFile failed: GetLastError=";
|
|
223
|
+
msg += std::to_string(GetLastError());
|
|
224
|
+
CloseHandle(mapping);
|
|
225
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
226
|
+
return env.Undefined();
|
|
227
|
+
}
|
|
228
|
+
ShmHandle* handle = new ShmHandle();
|
|
229
|
+
handle->ptr = ptr;
|
|
230
|
+
handle->byteLength = byteLength;
|
|
231
|
+
handle->mapping = mapping;
|
|
232
|
+
#else
|
|
233
|
+
int fd = shm_open(name.c_str(), O_RDWR, 0600);
|
|
234
|
+
if (fd < 0) {
|
|
235
|
+
std::string msg = "shm_open(open) failed: ";
|
|
236
|
+
msg += std::strerror(errno);
|
|
237
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
238
|
+
return env.Undefined();
|
|
239
|
+
}
|
|
240
|
+
// mmap() with a length past the real segment size succeeds; later
|
|
241
|
+
// access past st_size faults with an uncatchable SIGBUS. Reject an
|
|
242
|
+
// oversized request up front against the segment's actual size.
|
|
243
|
+
struct stat st;
|
|
244
|
+
if (fstat(fd, &st) != 0) {
|
|
245
|
+
std::string msg = "fstat failed: ";
|
|
246
|
+
msg += std::strerror(errno);
|
|
247
|
+
::close(fd);
|
|
248
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
249
|
+
return env.Undefined();
|
|
250
|
+
}
|
|
251
|
+
if (static_cast<uint64_t>(byteLength) >
|
|
252
|
+
static_cast<uint64_t>(st.st_size)) {
|
|
253
|
+
std::string msg = "open: requested byteLength ";
|
|
254
|
+
msg += std::to_string(static_cast<uint64_t>(byteLength));
|
|
255
|
+
msg += " exceeds segment size ";
|
|
256
|
+
msg += std::to_string(static_cast<uint64_t>(st.st_size));
|
|
257
|
+
::close(fd);
|
|
258
|
+
Napi::RangeError::New(env, msg).ThrowAsJavaScriptException();
|
|
259
|
+
return env.Undefined();
|
|
260
|
+
}
|
|
261
|
+
void* ptr = mmap(nullptr, byteLength, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
262
|
+
if (ptr == MAP_FAILED) {
|
|
263
|
+
std::string msg = "mmap failed: ";
|
|
264
|
+
msg += std::strerror(errno);
|
|
265
|
+
::close(fd);
|
|
266
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
267
|
+
return env.Undefined();
|
|
268
|
+
}
|
|
269
|
+
ShmHandle* handle = new ShmHandle();
|
|
270
|
+
handle->ptr = ptr;
|
|
271
|
+
handle->byteLength = byteLength;
|
|
272
|
+
handle->fd = fd;
|
|
273
|
+
#endif
|
|
274
|
+
|
|
275
|
+
return MakeSegment(env, handle);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// close(handle: External<ShmHandle>) -> void. Unmaps this process's view.
|
|
279
|
+
Napi::Value Close(const Napi::CallbackInfo& info) {
|
|
280
|
+
Napi::Env env = info.Env();
|
|
281
|
+
if (info.Length() < 1 || !info[0].IsExternal()) {
|
|
282
|
+
Napi::TypeError::New(env, "close(handle) expects a segment handle")
|
|
283
|
+
.ThrowAsJavaScriptException();
|
|
284
|
+
return env.Undefined();
|
|
285
|
+
}
|
|
286
|
+
ShmHandle* handle = info[0].As<Napi::External<ShmHandle>>().Data();
|
|
287
|
+
UnmapHandle(handle);
|
|
288
|
+
return env.Undefined();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// unlink(name: string) -> void. POSIX: shm_unlink. Windows: no-op (the OS
|
|
292
|
+
// reclaims the segment once the last mapping handle is closed).
|
|
293
|
+
Napi::Value Unlink(const Napi::CallbackInfo& info) {
|
|
294
|
+
Napi::Env env = info.Env();
|
|
295
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
296
|
+
Napi::TypeError::New(env, "unlink(name: string)")
|
|
297
|
+
.ThrowAsJavaScriptException();
|
|
298
|
+
return env.Undefined();
|
|
299
|
+
}
|
|
300
|
+
#ifndef _WIN32
|
|
301
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
302
|
+
if (shm_unlink(name.c_str()) != 0) {
|
|
303
|
+
std::string msg = "shm_unlink failed: ";
|
|
304
|
+
msg += std::strerror(errno);
|
|
305
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
306
|
+
}
|
|
307
|
+
#endif
|
|
308
|
+
return env.Undefined();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
312
|
+
exports.Set("create", Napi::Function::New(env, Create));
|
|
313
|
+
exports.Set("open", Napi::Function::New(env, Open));
|
|
314
|
+
exports.Set("close", Napi::Function::New(env, Close));
|
|
315
|
+
exports.Set("unlink", Napi::Function::New(env, Unlink));
|
|
316
|
+
return exports;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
} // namespace
|
|
320
|
+
|
|
321
|
+
NODE_API_MODULE(shm_ring, Init)
|