From ebfc89e072ab6de9d42f745ccf9340670f022e1f Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 14:06:42 -0600 Subject: [PATCH] zero copy blob --- archive/miniz.c | 25 +++++++++------- fd_playdate.c | 33 ++++++++------------- internal/fd.c | 50 +++++++++++++++++-------------- internal/kim.c | 29 +++++++++--------- net/socket.c | 59 +++++++++++++++++-------------------- playdate/file_playdate.c | 14 ++++----- playdate/network_playdate.c | 30 ++++++++----------- qop.c | 30 ++++++++----------- source/cell.h | 2 ++ source/runtime.c | 32 ++++++++++++++++---- tests/miniz.cm | 9 +++--- 11 files changed, 160 insertions(+), 153 deletions(-) diff --git a/archive/miniz.c b/archive/miniz.c index 65657c55..1f5295df 100644 --- a/archive/miniz.c +++ b/archive/miniz.c @@ -113,31 +113,34 @@ static JSValue js_miniz_compress(JSContext *js, JSValue this_val, return JS_EXCEPTION; } - /* ─── 2. Allocate an output buffer big enough ────────────── */ + /* ─── 2. Allocate output blob (before getting blob input ptr) ── */ mz_ulong out_len_est = mz_compressBound(in_len); - void *out_buf = js_malloc_rt(out_len_est); - if (!out_buf) { + void *out_ptr; + JSValue abuf = js_new_blob_alloc(js, (size_t)out_len_est, &out_ptr); + if (JS_IsException(abuf)) { if (cstring) JS_FreeCString(js, cstring); - return JS_EXCEPTION; + return abuf; + } + + /* Re-derive blob input pointer after alloc (GC may have moved it) */ + if (!cstring) { + in_ptr = js_get_blob_data(js, &in_len, argv[0]); } /* ─── 3. Do the compression (MZ_DEFAULT_COMPRESSION = level 6) */ mz_ulong out_len = out_len_est; - int st = mz_compress2(out_buf, &out_len, + int st = mz_compress2(out_ptr, &out_len, in_ptr, in_len, MZ_DEFAULT_COMPRESSION); /* clean-up for string input */ if (cstring) JS_FreeCString(js, cstring); - if (st != MZ_OK) { - js_free_rt(out_buf); + if (st != MZ_OK) return JS_RaiseDisrupt(js, "miniz: compression failed (%d)", st); - } - /* ─── 4. Hand JavaScript a copy of the compressed data ────── */ - JSValue abuf = js_new_blob_stoned_copy(js, out_buf, out_len); - js_free_rt(out_buf); + /* ─── 4. Stone with actual compressed size ────────────────── */ + js_blob_stone(abuf, (size_t)out_len); return abuf; } diff --git a/fd_playdate.c b/fd_playdate.c index fa1759c8..bb019c1b 100644 --- a/fd_playdate.c +++ b/fd_playdate.c @@ -98,19 +98,17 @@ JSC_CCALL(fd_read, if (argc > 1) size = js2number(js, argv[1]); - void *buf = malloc(size); - if (!buf) - return JS_RaiseDisrupt(js, "malloc failed"); - - int bytes_read = pd_file->read(fd, buf, (unsigned int)size); + void *out; + ret = js_new_blob_alloc(js, size, &out); + if (JS_IsException(ret)) return ret; + + int bytes_read = pd_file->read(fd, out, (unsigned int)size); if (bytes_read < 0) { - free(buf); const char* err = pd_file->geterr(); return JS_RaiseDisrupt(js, "read failed: %s", err ? err : "unknown error"); } - - ret = js_new_blob_stoned_copy(js, buf, bytes_read); - free(buf); + + js_blob_stone(ret, bytes_read); return ret; ) @@ -134,22 +132,17 @@ JSC_SCALL(fd_slurp, return JS_RaiseDisrupt(js, "open failed: %s", err ? err : "unknown error"); } - void *data = malloc(size); - if (!data) { - pd_file->close(fd); - return JS_RaiseDisrupt(js, "malloc failed"); - } + void *out; + ret = js_new_blob_alloc(js, size, &out); + if (JS_IsException(ret)) { pd_file->close(fd); return ret; } - int bytes_read = pd_file->read(fd, data, (unsigned int)size); + int bytes_read = pd_file->read(fd, out, (unsigned int)size); pd_file->close(fd); - if (bytes_read < 0) { - free(data); + if (bytes_read < 0) return JS_RaiseDisrupt(js, "read failed"); - } - ret = js_new_blob_stoned_copy(js, data, bytes_read); - free(data); + js_blob_stone(ret, bytes_read); ) JSC_CCALL(fd_lseek, diff --git a/internal/fd.c b/internal/fd.c index 657a80ae..02b97519 100644 --- a/internal/fd.c +++ b/internal/fd.c @@ -102,18 +102,15 @@ JSC_CCALL(fd_read, if (argc > 1) size = js2number(js, argv[1]); - void *buf = malloc(size); - if (!buf) - return JS_RaiseDisrupt(js, "malloc failed"); - - ssize_t bytes_read = read(fd, buf, size); - if (bytes_read < 0) { - free(buf); + void *out; + ret = js_new_blob_alloc(js, size, &out); + if (JS_IsException(ret)) return ret; + + ssize_t bytes_read = read(fd, out, size); + if (bytes_read < 0) return JS_RaiseDisrupt(js, "read failed: %s", strerror(errno)); - } - - ret = js_new_blob_stoned_copy(js, buf, bytes_read); - free(buf); + + js_blob_stone(ret, bytes_read); return ret; ) @@ -128,39 +125,48 @@ JSC_SCALL(fd_slurp, size_t size = st.st_size; if (size == 0) return js_new_blob_stoned_copy(js, NULL, 0); - + #ifndef _WIN32 int fd = open(str, O_RDONLY); if (fd < 0) return JS_RaiseDisrupt(js, "open failed: %s", strerror(errno)); - - void *data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); - if (data == MAP_FAILED) { - close(fd); - return JS_RaiseDisrupt(js, "mmap failed: %s", strerror(errno)); + + void *out; + ret = js_new_blob_alloc(js, size, &out); + if (JS_IsException(ret)) { close(fd); return ret; } + + size_t total = 0; + while (total < size) { + ssize_t n = read(fd, (uint8_t *)out + total, size - total); + if (n < 0) { + if (errno == EINTR) continue; + close(fd); + return JS_RaiseDisrupt(js, "read failed: %s", strerror(errno)); + } + if (n == 0) break; + total += n; } - ret = js_new_blob_stoned_copy(js, data, size); - munmap(data, size); close(fd); + js_blob_stone(ret, total); #else // Windows: use memory mapping for optimal performance HANDLE hFile = CreateFileA(str, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) return JS_RaiseDisrupt(js, "CreateFile failed: %lu", GetLastError()); - + HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (hMapping == NULL) { CloseHandle(hFile); return JS_RaiseDisrupt(js, "CreateFileMapping failed: %lu", GetLastError()); } - + void *data = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); if (data == NULL) { CloseHandle(hMapping); CloseHandle(hFile); return JS_RaiseDisrupt(js, "MapViewOfFile failed: %lu", GetLastError()); } - + ret = js_new_blob_stoned_copy(js, data, size); UnmapViewOfFile(data); CloseHandle(hMapping); diff --git a/internal/kim.c b/internal/kim.c index 94139373..2b633e50 100644 --- a/internal/kim.c +++ b/internal/kim.c @@ -8,26 +8,25 @@ JSC_CCALL(kim_encode, const char *utf8_str = JS_ToCString(js, argv[0]); if (!utf8_str) return JS_EXCEPTION; - + // Count runes to estimate kim buffer size int rune_count = utf8_count(utf8_str); - - // Allocate kim buffer (worst case: 5 bytes per rune) + + // Allocate blob (worst case: 5 bytes per rune) size_t kim_size = rune_count * 5; - char *kim_buffer = malloc(kim_size); - char *kim_ptr = kim_buffer; - - // Encode utf8 to kim + void *out; + ret = js_new_blob_alloc(js, kim_size, &out); + if (JS_IsException(ret)) { JS_FreeCString(js, utf8_str); return ret; } + + // Encode utf8 to kim directly into blob + char *kim_ptr = (char *)out; long long runes_encoded; utf8_to_kim(&utf8_str, &kim_ptr, &runes_encoded); - - // Calculate actual size used - size_t actual_size = kim_ptr - kim_buffer; - - // Create blob with the encoded data - ret = js_new_blob_stoned_copy(js, kim_buffer, actual_size); - - free(kim_buffer); + + // Stone with actual size used + size_t actual_size = kim_ptr - (char *)out; + js_blob_stone(ret, actual_size); + JS_FreeCString(js, utf8_str); ) diff --git a/net/socket.c b/net/socket.c index 2a1e8d6c..ac8ad204 100644 --- a/net/socket.c +++ b/net/socket.c @@ -333,19 +333,15 @@ JSC_CCALL(socket_recv, flags = js2number(js, argv[2]); } - void *buf = malloc(len); - if (!buf) { - return JS_RaiseDisrupt(js, "malloc failed"); - } - - ssize_t received = recv(sockfd, buf, len, flags); - if (received < 0) { - free(buf); + void *out; + ret = js_new_blob_alloc(js, len, &out); + if (JS_IsException(ret)) return ret; + + ssize_t received = recv(sockfd, out, len, flags); + if (received < 0) return JS_RaiseDisrupt(js, "recv failed: %s", strerror(errno)); - } - - ret = js_new_blob_stoned_copy(js, buf, received); - free(buf); + + js_blob_stone(ret, received); return ret; ) @@ -421,25 +417,20 @@ JSC_CCALL(socket_recvfrom, flags = js2number(js, argv[2]); } - void *buf = malloc(len); - if (!buf) { - return JS_RaiseDisrupt(js, "malloc failed"); - } - + void *out; + JSValue blob = js_new_blob_alloc(js, len, &out); + if (JS_IsException(blob)) return blob; + struct sockaddr_storage from_addr; socklen_t from_len = sizeof from_addr; - - ssize_t received = recvfrom(sockfd, buf, len, flags, + + ssize_t received = recvfrom(sockfd, out, len, flags, (struct sockaddr *)&from_addr, &from_len); - if (received < 0) { - free(buf); + if (received < 0) return JS_RaiseDisrupt(js, "recvfrom failed: %s", strerror(errno)); - } - - ret = JS_NewObject(js); - JS_SetPropertyStr(js, ret, "data", js_new_blob_stoned_copy(js, buf, received)); - free(buf); - + + js_blob_stone(blob, received); + // Get source address info char ipstr[INET6_ADDRSTRLEN]; int port; @@ -452,11 +443,15 @@ JSC_CCALL(socket_recvfrom, inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof ipstr); port = ntohs(s->sin6_port); } - - JSValue addr_info = JS_NewObject(js); - JS_SetPropertyStr(js, addr_info, "address", JS_NewString(js, ipstr)); - JS_SetPropertyStr(js, addr_info, "port", JS_NewInt32(js, port)); - JS_SetPropertyStr(js, ret, "address", addr_info); + + JS_FRAME(js); + JS_ROOT(ret_r, JS_NewObject(js)); + JS_SetPropertyStr(js, ret_r.val, "data", blob); + JS_ROOT(addr_info, JS_NewObject(js)); + JS_SetPropertyStr(js, addr_info.val, "address", JS_NewString(js, ipstr)); + JS_SetPropertyStr(js, addr_info.val, "port", JS_NewInt32(js, port)); + JS_SetPropertyStr(js, ret_r.val, "address", addr_info.val); + JS_RETURN(ret_r.val); ) JSC_CCALL(socket_shutdown, diff --git a/playdate/file_playdate.c b/playdate/file_playdate.c index b3500e62..38064ba6 100644 --- a/playdate/file_playdate.c +++ b/playdate/file_playdate.c @@ -78,15 +78,13 @@ JSC_CCALL(file_read, if (!pd_file) return JS_RaiseDisrupt(js, "file not initialized"); SDFile *f = js2sdfile(js, argv[0]); unsigned int len = (unsigned int)js2number(js, argv[1]); - void *buf = malloc(len); - if (!buf) return JS_RaiseDisrupt(js, "malloc failed"); - int read = pd_file->read(f, buf, len); - if (read < 0) { - free(buf); + void *out; + JSValue blob = js_new_blob_alloc(js, len, &out); + if (JS_IsException(blob)) return blob; + int bytes_read = pd_file->read(f, out, len); + if (bytes_read < 0) return JS_NULL; - } - JSValue blob = js_new_blob_stoned_copy(js, buf, read); - free(buf); + js_blob_stone(blob, bytes_read); return blob; ) diff --git a/playdate/network_playdate.c b/playdate/network_playdate.c index ae03fd7a..d4bedc0c 100644 --- a/playdate/network_playdate.c +++ b/playdate/network_playdate.c @@ -139,15 +139,13 @@ JSC_CCALL(http_read, if (!pd_network || !pd_network->http) return JS_RaiseDisrupt(js, "network not initialized"); HTTPConnection *conn = js2http(js, argv[0]); unsigned int buflen = (unsigned int)js2number(js, argv[1]); - void *buf = malloc(buflen); - if (!buf) return JS_RaiseDisrupt(js, "malloc failed"); - int read = pd_network->http->read(conn, buf, buflen); - if (read < 0) { - free(buf); + void *out; + JSValue blob = js_new_blob_alloc(js, buflen, &out); + if (JS_IsException(blob)) return blob; + int bytes_read = pd_network->http->read(conn, out, buflen); + if (bytes_read < 0) return JS_NULL; - } - JSValue blob = js_new_blob_stoned_copy(js, buf, read); - free(buf); + js_blob_stone(blob, bytes_read); return blob; ) @@ -218,15 +216,13 @@ JSC_CCALL(tcp_read, if (!pd_network || !pd_network->tcp) return JS_RaiseDisrupt(js, "network not initialized"); TCPConnection *conn = js2tcp(js, argv[0]); size_t len = (size_t)js2number(js, argv[1]); - void *buf = malloc(len); - if (!buf) return JS_RaiseDisrupt(js, "malloc failed"); - int read = pd_network->tcp->read(conn, buf, len); - if (read < 0) { - free(buf); - return JS_NewInt32(js, read); // Return error code - } - JSValue blob = js_new_blob_stoned_copy(js, buf, read); - free(buf); + void *out; + JSValue blob = js_new_blob_alloc(js, len, &out); + if (JS_IsException(blob)) return blob; + int bytes_read = pd_network->tcp->read(conn, out, len); + if (bytes_read < 0) + return JS_NewInt32(js, bytes_read); // Return error code + js_blob_stone(blob, bytes_read); return blob; ) diff --git a/qop.c b/qop.c index 222509ff..cf3e2316 100644 --- a/qop.c +++ b/qop.c @@ -183,18 +183,15 @@ static JSValue js_qop_read(JSContext *js, JSValue self, int argc, JSValue *argv) return JS_NULL; } - unsigned char *dest = js_malloc_rt(file->size); - if (!dest) - return JS_RaiseOOM(js); + void *out; + JSValue blob = js_new_blob_alloc(js, file->size, &out); + if (JS_IsException(blob)) return blob; - int bytes = qop_read(qop, file, dest); - if (bytes == 0) { - js_free_rt(dest); + int bytes = qop_read(qop, file, out); + if (bytes == 0) return JS_RaiseDisrupt(js, "Failed to read file"); - } - JSValue blob = js_new_blob_stoned_copy(js, dest, bytes); - js_free_rt(dest); + js_blob_stone(blob, bytes); return blob; } @@ -223,18 +220,15 @@ static JSValue js_qop_read_ex(JSContext *js, JSValue self, int argc, JSValue *ar if (JS_ToUint32(js, &start, argv[1]) < 0 || JS_ToUint32(js, &len, argv[2]) < 0) return JS_RaiseDisrupt(js, "Invalid start or len"); - unsigned char *dest = js_malloc_rt(len); - if (!dest) - return JS_RaiseOOM(js); + void *out; + JSValue blob = js_new_blob_alloc(js, len, &out); + if (JS_IsException(blob)) return blob; - int bytes = qop_read_ex(qop, file, dest, start, len); - if (bytes == 0) { - js_free_rt(dest); + int bytes = qop_read_ex(qop, file, out, start, len); + if (bytes == 0) return JS_RaiseDisrupt(js, "Failed to read file part"); - } - JSValue blob = js_new_blob_stoned_copy(js, dest, bytes); - js_free_rt(dest); + js_blob_stone(blob, bytes); return blob; } diff --git a/source/cell.h b/source/cell.h index af4d965c..1f0692ae 100644 --- a/source/cell.h +++ b/source/cell.h @@ -10,6 +10,8 @@ extern "C" { // blob fns JSValue js_core_blob_use(JSContext *js); +JSValue js_new_blob_alloc(JSContext *js, size_t bytes, void **out); +void js_blob_stone(JSValue blob, size_t actual_bytes); JSValue js_new_blob_stoned_copy(JSContext *js, void *data, size_t bytes); void *js_get_blob_data(JSContext *js, size_t *size, JSValue v); // bytes void *js_get_blob_data_bits(JSContext *js, size_t *bits, JSValue v); // bits diff --git a/source/runtime.c b/source/runtime.c index 920d0754..4415572b 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -10048,16 +10048,38 @@ JSValue js_core_blob_use (JSContext *js) { return JS_GetPropertyStr (js, js->global_obj, "blob"); } -/* Create a new blob from raw data, stone it, and return as JSValue */ -JSValue js_new_blob_stoned_copy (JSContext *js, void *data, size_t bytes) { +/* Allocate a mutable blob. *out receives writable pointer to data area. + WARNING: *out is invalidated by ANY GC-triggering operation. + Caller fills data, then calls js_blob_stone(). */ +JSValue js_new_blob_alloc (JSContext *js, size_t bytes, void **out) { size_t bits = bytes * 8; JSValue bv = js_new_heap_blob (js, bits); - if (JS_IsException (bv)) return bv; + if (JS_IsException (bv)) { + *out = NULL; + return bv; + } JSBlob *bd = (JSBlob *)chase (bv); - if (bytes > 0) - memcpy (bd->bits, data, bytes); bd->length = bits; + *out = bd->bits; + return bv; +} + +/* Set actual length and stone the blob. actual_bytes <= allocated bytes. + Does NOT allocate — cannot trigger GC. */ +void js_blob_stone (JSValue blob, size_t actual_bytes) { + JSBlob *bd = (JSBlob *)chase (blob); + bd->length = actual_bytes * 8; bd->mist_hdr = objhdr_set_s (bd->mist_hdr, true); +} + +/* Create a new blob from raw data, stone it, and return as JSValue */ +JSValue js_new_blob_stoned_copy (JSContext *js, void *data, size_t bytes) { + void *out; + JSValue bv = js_new_blob_alloc (js, bytes, &out); + if (JS_IsException (bv)) return bv; + if (bytes > 0) + memcpy (out, data, bytes); + js_blob_stone (bv, bytes); return bv; } diff --git a/tests/miniz.cm b/tests/miniz.cm index cb8840ad..fcd70362 100644 --- a/tests/miniz.cm +++ b/tests/miniz.cm @@ -1,6 +1,5 @@ var fd = use("fd") var miniz = use("miniz") -var utf8 = use("utf8") return { create_and_read_zip: function() { @@ -35,7 +34,7 @@ return { disrupt extracted_blob = reader.slurp(ENTRY_PATH) - extracted_text = utf8.decode(extracted_blob) + extracted_text = text(extracted_blob) if (extracted_text != PAYLOAD) disrupt @@ -59,8 +58,8 @@ return { var _run = function() { writer = miniz.write(ZIP_PATH) - writer.add_file(ENTRY1, utf8.encode("content1")) - writer.add_file(ENTRY2, utf8.encode("content2")) + writer.add_file(ENTRY1, blob("content1")) + writer.add_file(ENTRY2, blob("content2")) writer = null zip_blob = fd.slurp(ZIP_PATH) @@ -87,7 +86,7 @@ return { var _run = function() { writer = miniz.write(ZIP_PATH) - writer.add_file(ENTRY_PATH, utf8.encode("data")) + writer.add_file(ENTRY_PATH, blob("data")) writer = null zip_blob = fd.slurp(ZIP_PATH)