From e50d596c77b670b86136e8bc6d6d85a7f88b95ff Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 21 Oct 2026 16:05:13 -0300 Subject: [PATCH] perf(runtime): marshal C string arguments with a single allocation Each string passed to a char* parameter went through String::Utf8Value (one allocation), strdup (a second allocation and copy) and a separately allocated OneByteStringResource. The resource now owns its bytes inline: NewUtf8Copy sizes one block for the resource plus the UTF-8 copy and has V8 write straight into it, with the same flags Utf8Value uses, and Dispose() frees the block. The external string that ties the copy's lifetime to the GC is unchanged, so a pointer into the argument stays valid exactly as long as before. --- NativeScript/runtime/Interop.mm | 11 +++---- .../runtime/OneByteStringResource.cpp | 25 +++++++++++---- NativeScript/runtime/OneByteStringResource.h | 31 +++++++++++++------ 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index ac140efad..196714558 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -365,16 +365,13 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Local()); + // The external string only ties the copy's lifetime to the GC; it is + // never read as a string. bool success = v8::String::NewExternalOneByte(isolate, resource).ToLocal(&arg); tns::Assert(success, isolate); - Interop::SetValue(dest, value); + Interop::SetValue(dest, resource->data()); } else { BaseDataWrapper* wrapper = tns::GetValue(isolate, arg); if (wrapper == nullptr) { diff --git a/NativeScript/runtime/OneByteStringResource.cpp b/NativeScript/runtime/OneByteStringResource.cpp index e10086cab..b8d724e32 100644 --- a/NativeScript/runtime/OneByteStringResource.cpp +++ b/NativeScript/runtime/OneByteStringResource.cpp @@ -1,22 +1,35 @@ #include "OneByteStringResource.h" #include +#include using namespace v8; namespace tns { -OneByteStringResource::OneByteStringResource(const char* data, size_t length): - data_(data), length_(length) { +OneByteStringResource* OneByteStringResource::NewUtf8Copy(Isolate* isolate, + Local str) { + size_t byteLength = str->Utf8LengthV2(isolate); + void* block = std::malloc(sizeof(OneByteStringResource) + byteLength + 1); + OneByteStringResource* resource = + new (block) OneByteStringResource(byteLength + 1); + // Same flags as String::Utf8Value. + str->WriteUtf8V2(isolate, reinterpret_cast(resource + 1), + byteLength + 1, + String::WriteFlags::kNullTerminate | + String::WriteFlags::kReplaceInvalidUtf8); + return resource; } -OneByteStringResource::~OneByteStringResource() { - // data_ comes from strdup (see Interop::WriteValue's CStringEncoding path). - std::free(const_cast(this->data_)); +OneByteStringResource::OneByteStringResource(size_t length) : length_(length) {} + +void OneByteStringResource::Dispose() { + this->~OneByteStringResource(); + std::free(this); } const char* OneByteStringResource::data() const { - return this->data_; + return reinterpret_cast(this + 1); } size_t OneByteStringResource::length() const { diff --git a/NativeScript/runtime/OneByteStringResource.h b/NativeScript/runtime/OneByteStringResource.h index 0cf0a8fe9..e49a572dd 100644 --- a/NativeScript/runtime/OneByteStringResource.h +++ b/NativeScript/runtime/OneByteStringResource.h @@ -5,17 +5,28 @@ namespace tns { -class OneByteStringResource : public v8::String::ExternalOneByteStringResource { -public: - OneByteStringResource(const char* data, size_t length); - ~OneByteStringResource() override; - const char* data() const override; - size_t length() const override; -private: - const char* data_; - size_t length_; -}; +// A NUL-terminated UTF-8 copy of a JS string, stored in the same allocation as +// the resource. V8 releases it through Dispose(), never through delete. +class OneByteStringResource final + : public v8::String::ExternalOneByteStringResource { + public: + static OneByteStringResource* NewUtf8Copy(v8::Isolate* isolate, + v8::Local str); + + const char* data() const override; + // The UTF-8 byte count plus the NUL, so it is never zero: V8 disposes a + // zero-length resource as soon as it is handed one. + size_t length() const override; + + protected: + void Dispose() override; + private: + explicit OneByteStringResource(size_t length); + ~OneByteStringResource() override = default; + + size_t length_; +}; } #endif /* OneByteStringResource_h */