@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/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)