MMap native library

This commit is contained in:
eli
2026-01-26 08:05:00 +00:00
commit 033bbe2512
9 changed files with 1749 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
build/
dist/
*.log
*.node
.DS_Store

6
.npmignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
build/
*.log
.DS_Store
test.ts
tsconfig.json

25
binding.gyp Normal file
View File

@@ -0,0 +1,25 @@
# binding.gyp
{
"targets": [
{
"target_name": "mmap_binding",
"sources": ["src/mmap_binding.cc"],
"include_dirs": [],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"cflags_cc": ["-std=c++20", "-fexceptions"],
"conditions": [
["OS=='mac'", {
"xcode_settings": {
"CLANG_CXX_LANGUAGE_STANDARD": "c++20",
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"MACOSX_DEPLOYMENT_TARGET": "10.15"
}
}],
["OS=='linux'", {
"cflags_cc": ["-std=c++20"]
}]
]
}
]
}

89
lib/index.ts Normal file
View File

@@ -0,0 +1,89 @@
// lib/index.ts
import { createRequire } from 'node:module';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
// dist/lib/index.js 에서 실행될 때 -> ../../build/Release/
// lib/index.ts 에서 실행될 때 -> ../build/Release/
// 둘 다 지원하도록 동적으로 찾기
function findBinding(): string {
const candidates = [
path.join(__dirname, '..', '..', 'build', 'Release', 'mmap_binding.node'), // dist/lib/ 기준
path.join(__dirname, '..', 'build', 'Release', 'mmap_binding.node'), // lib/ 기준
];
for (const candidate of candidates) {
try {
require.resolve(candidate);
return candidate;
} catch {
continue;
}
}
throw new Error('Cannot find mmap_binding.node. Run `npm run build:native` first.');
}
const binding = require(findBinding());
// 타입 정의
export interface MmapBinding {
map(size: number, prot: number, flags: number, fd: number, offset: number): Buffer;
sync(buffer: Buffer, offset: number, length: number, flags: number): void;
unmap(buffer: Buffer): void;
advise(buffer: Buffer, offset: number, length: number, advice: number): void;
pageSize(): number;
PROT_NONE: number;
PROT_READ: number;
PROT_WRITE: number;
PROT_EXEC: number;
MAP_SHARED: number;
MAP_PRIVATE: number;
MAP_ANONYMOUS?: number;
MAP_ANON?: number;
MS_ASYNC: number;
MS_SYNC: number;
MS_INVALIDATE: number;
MADV_NORMAL: number;
MADV_RANDOM: number;
MADV_SEQUENTIAL: number;
MADV_WILLNEED: number;
MADV_DONTNEED: number;
}
const mmap: MmapBinding = binding;
export default mmap;
export const {
map,
sync,
unmap,
advise,
pageSize,
PROT_NONE,
PROT_READ,
PROT_WRITE,
PROT_EXEC,
MAP_SHARED,
MAP_PRIVATE,
MAP_ANONYMOUS,
MAP_ANON,
MS_ASYNC,
MS_SYNC,
MS_INVALIDATE,
MADV_NORMAL,
MADV_RANDOM,
MADV_SEQUENTIAL,
MADV_WILLNEED,
MADV_DONTNEED,
} = mmap;

1282
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@elilee/mmap-native",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "module",
"main": "./dist/lib/index.js",
"types": "./dist/lib/index.d.ts",
"scripts": {
"install": "node-gyp rebuild",
"build:native": "node-gyp rebuild",
"build:ts": "tsc",
"build": "npm run build:native && npm run build:ts",
"clean": "node-gyp clean && rm -rf dist"
},
"devDependencies": {
"@types/node": "^22.0.0",
"node-gyp": "^11.0.0",
"typescript": "^5.7.0"
},
"overrides": {
"tar": "^7.0.0",
"glob": "^10.0.0",
"rimraf": "^5.0.0"
},
"engines": {
"node": ">=18.0.0"
},
"os": ["linux", "darwin"],
"cpu": ["x64", "arm64"]
}

253
src/mmap_binding.cc Normal file
View File

@@ -0,0 +1,253 @@
// src/mmap_binding.cc
#define NAPI_VERSION 8
#include <node_api.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <cstdio>
#include <unordered_map>
#include <mutex>
// ============== 에러 처리 매크로 ==============
#define NAPI_CALL(env, call) \
do { \
napi_status status = (call); \
if (status != napi_ok) { \
const napi_extended_error_info* error_info = nullptr; \
napi_get_last_error_info((env), &error_info); \
const char* msg = (error_info && error_info->error_message) \
? error_info->error_message \
: "Unknown N-API error"; \
napi_throw_error((env), nullptr, msg); \
return nullptr; \
} \
} while (0)
#define THROW_ERRNO(env, prefix) \
do { \
char buf[256]; \
std::snprintf(buf, sizeof(buf), "%s: %s", prefix, std::strerror(errno)); \
napi_throw_error(env, nullptr, buf); \
return nullptr; \
} while (0)
// ============== 매핑 정보 저장 ==============
struct MmapInfo {
void* addr;
size_t length;
int fd;
bool owns_fd;
};
static std::unordered_map<void*, MmapInfo> g_mappings;
static std::mutex g_mutex;
// ============== Buffer Destructor (GC 시 자동 unmap) ==============
static void buffer_release_callback(napi_env env, void* data, void* hint) {
std::lock_guard<std::mutex> lock(g_mutex);
auto it = g_mappings.find(data);
if (it != g_mappings.end()) {
munmap(it->second.addr, it->second.length);
if (it->second.owns_fd && it->second.fd >= 0) {
close(it->second.fd);
}
g_mappings.erase(it);
}
}
// ============== mmap(size, prot, flags, fd, offset) -> Buffer ==============
static napi_value MmapMap(napi_env env, napi_callback_info info) {
size_t argc = 5;
napi_value args[5];
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
if (argc < 5) {
napi_throw_error(env, nullptr, "mmap.map requires 5 arguments: size, prot, flags, fd, offset");
return nullptr;
}
int64_t size, prot, flags, fd, offset;
NAPI_CALL(env, napi_get_value_int64(env, args[0], &size));
NAPI_CALL(env, napi_get_value_int64(env, args[1], &prot));
NAPI_CALL(env, napi_get_value_int64(env, args[2], &flags));
NAPI_CALL(env, napi_get_value_int64(env, args[3], &fd));
NAPI_CALL(env, napi_get_value_int64(env, args[4], &offset));
void* addr = mmap(nullptr, static_cast<size_t>(size),
static_cast<int>(prot), static_cast<int>(flags),
static_cast<int>(fd), static_cast<off_t>(offset));
if (addr == MAP_FAILED) {
THROW_ERRNO(env, "mmap failed");
}
{
std::lock_guard<std::mutex> lock(g_mutex);
g_mappings[addr] = {addr, static_cast<size_t>(size), static_cast<int>(fd), false};
}
napi_value buffer;
NAPI_CALL(env, napi_create_external_buffer(env, static_cast<size_t>(size),
addr, buffer_release_callback,
nullptr, &buffer));
return buffer;
}
// ============== msync(buffer, offset, length, flags) ==============
static napi_value MmapSync(napi_env env, napi_callback_info info) {
size_t argc = 4;
napi_value args[4];
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
if (argc < 4) {
napi_throw_error(env, nullptr, "mmap.sync requires 4 arguments: buffer, offset, length, flags");
return nullptr;
}
void* data;
size_t buffer_length;
NAPI_CALL(env, napi_get_buffer_info(env, args[0], &data, &buffer_length));
int64_t offset, length, flags;
NAPI_CALL(env, napi_get_value_int64(env, args[1], &offset));
NAPI_CALL(env, napi_get_value_int64(env, args[2], &length));
NAPI_CALL(env, napi_get_value_int64(env, args[3], &flags));
if (offset < 0 || length < 0 || static_cast<size_t>(offset + length) > buffer_length) {
napi_throw_range_error(env, nullptr, "Invalid offset/length for msync");
return nullptr;
}
char* sync_addr = static_cast<char*>(data) + offset;
if (msync(sync_addr, static_cast<size_t>(length), static_cast<int>(flags)) != 0) {
THROW_ERRNO(env, "msync failed");
}
napi_value undefined;
NAPI_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}
// ============== munmap(buffer) ==============
static napi_value MmapUnmap(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
if (argc < 1) {
napi_throw_error(env, nullptr, "mmap.unmap requires 1 argument: buffer");
return nullptr;
}
void* data;
size_t length;
NAPI_CALL(env, napi_get_buffer_info(env, args[0], &data, &length));
std::lock_guard<std::mutex> lock(g_mutex);
auto it = g_mappings.find(data);
if (it != g_mappings.end()) {
if (munmap(it->second.addr, it->second.length) != 0) {
THROW_ERRNO(env, "munmap failed");
}
g_mappings.erase(it);
}
napi_value undefined;
NAPI_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}
// ============== madvise(buffer, offset, length, advice) ==============
static napi_value MmapAdvise(napi_env env, napi_callback_info info) {
size_t argc = 4;
napi_value args[4];
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
if (argc < 4) {
napi_throw_error(env, nullptr, "mmap.advise requires 4 arguments: buffer, offset, length, advice");
return nullptr;
}
void* data;
size_t buffer_length;
NAPI_CALL(env, napi_get_buffer_info(env, args[0], &data, &buffer_length));
int64_t offset, length, advice;
NAPI_CALL(env, napi_get_value_int64(env, args[1], &offset));
NAPI_CALL(env, napi_get_value_int64(env, args[2], &length));
NAPI_CALL(env, napi_get_value_int64(env, args[3], &advice));
char* advise_addr = static_cast<char*>(data) + offset;
if (madvise(advise_addr, static_cast<size_t>(length), static_cast<int>(advice)) != 0) {
THROW_ERRNO(env, "madvise failed");
}
napi_value undefined;
NAPI_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}
// ============== pageSize() -> number ==============
static napi_value GetPageSize(napi_env env, napi_callback_info info) {
napi_value result;
NAPI_CALL(env, napi_create_int64(env, sysconf(_SC_PAGESIZE), &result));
return result;
}
// ============== 상수 정의 헬퍼 ==============
static void DefineConstant(napi_env env, napi_value exports, const char* name, int64_t value) {
napi_value val;
napi_create_int64(env, value, &val);
napi_set_named_property(env, exports, name, val);
}
// ============== 모듈 초기화 ==============
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor props[] = {
{"map", nullptr, MmapMap, nullptr, nullptr, nullptr, napi_default, nullptr},
{"sync", nullptr, MmapSync, nullptr, nullptr, nullptr, napi_default, nullptr},
{"unmap", nullptr, MmapUnmap, nullptr, nullptr, nullptr, napi_default, nullptr},
{"advise", nullptr, MmapAdvise, nullptr, nullptr, nullptr, napi_default, nullptr},
{"pageSize", nullptr, GetPageSize, nullptr, nullptr, nullptr, napi_default, nullptr},
};
NAPI_CALL(env, napi_define_properties(env, exports, sizeof(props) / sizeof(props[0]), props));
// Protection flags
DefineConstant(env, exports, "PROT_NONE", PROT_NONE);
DefineConstant(env, exports, "PROT_READ", PROT_READ);
DefineConstant(env, exports, "PROT_WRITE", PROT_WRITE);
DefineConstant(env, exports, "PROT_EXEC", PROT_EXEC);
// Map flags
DefineConstant(env, exports, "MAP_SHARED", MAP_SHARED);
DefineConstant(env, exports, "MAP_PRIVATE", MAP_PRIVATE);
#ifdef MAP_ANONYMOUS
DefineConstant(env, exports, "MAP_ANONYMOUS", MAP_ANONYMOUS);
#endif
#ifdef MAP_ANON
DefineConstant(env, exports, "MAP_ANON", MAP_ANON);
#endif
// Sync flags
DefineConstant(env, exports, "MS_ASYNC", MS_ASYNC);
DefineConstant(env, exports, "MS_SYNC", MS_SYNC);
DefineConstant(env, exports, "MS_INVALIDATE", MS_INVALIDATE);
// Advise flags
DefineConstant(env, exports, "MADV_NORMAL", MADV_NORMAL);
DefineConstant(env, exports, "MADV_RANDOM", MADV_RANDOM);
DefineConstant(env, exports, "MADV_SEQUENTIAL", MADV_SEQUENTIAL);
DefineConstant(env, exports, "MADV_WILLNEED", MADV_WILLNEED);
DefineConstant(env, exports, "MADV_DONTNEED", MADV_DONTNEED);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

40
test.ts Normal file
View File

@@ -0,0 +1,40 @@
// test.ts
import * as fs from 'node:fs';
import mmap from './lib/index.js';
const FILE_PATH = './test_mmap.bin';
const FILE_SIZE = 4096;
// 파일 생성
const fd = fs.openSync(FILE_PATH, 'w+');
fs.ftruncateSync(fd, FILE_SIZE);
// mmap 매핑
const buffer = mmap.map(
FILE_SIZE,
mmap.PROT_READ | mmap.PROT_WRITE,
mmap.MAP_SHARED,
fd,
0
);
console.log('Mapped buffer length:', buffer.length);
console.log('Page size:', mmap.pageSize());
// 데이터 쓰기
buffer.write('Hello, mmap!', 0, 'utf8');
buffer.writeUInt32LE(12345, 100);
// msync
mmap.sync(buffer, 0, FILE_SIZE, mmap.MS_SYNC);
console.log('Data synced to disk');
// 데이터 읽기
console.log('String:', buffer.toString('utf8', 0, 12));
console.log('Number:', buffer.readUInt32LE(100));
// 정리
fs.closeSync(fd);
fs.unlinkSync(FILE_PATH);
console.log('Test passed!');

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["lib/**/*", "test.ts"],
"exclude": ["node_modules", "dist", "build"]
}