diff --git a/NativeScript/runtime/ArgConverter.h b/NativeScript/runtime/ArgConverter.h index 62bc7bb4..5e918109 100644 --- a/NativeScript/runtime/ArgConverter.h +++ b/NativeScript/runtime/ArgConverter.h @@ -66,7 +66,7 @@ class ArgConverter { // recycled the address and give it a foreign prototype. static std::shared_ptr> FindCachedInstance( v8::Isolate* isolate, const std::shared_ptr& cache, id target); - static const Meta* GetMeta(std::string name); + static const Meta* GetMeta(const std::string& name); static const ProtocolMeta* FindProtocolMeta(Protocol* protocol); static void MethodCallback(ffi_cif* cif, void* retValue, void** argValues, void* userData); diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index 0615fef9..56f1ab2a 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -61,9 +61,8 @@ ObjCDataWrapper* objcWrapper = static_cast(wrapper); target = objcWrapper->Data(); - std::string className = object_getClassName(target); auto cache = Caches::Get(isolate); - auto it = cache->ClassPrototypes.find(className); + auto it = cache->ClassPrototypes.find(std::string_view(object_getClassName(target))); // For extended classes we will call the base method callSuper = isMethodCallback && it != cache->ClassPrototypes.end(); } else { @@ -910,8 +909,7 @@ Class klass = [target class]; const Meta* meta = FindMeta(klass, typeEncoding); if (meta != nullptr) { - std::string className = object_getClassName(target); - auto it = cache->ClassPrototypes.find(className); + auto it = cache->ClassPrototypes.find(std::string_view(object_getClassName(target))); if (it != cache->ClassPrototypes.end()) { // for debugging rlv cell handling: // NSString* message = [NSString stringWithFormat:@"ArgConverter::CreateJsWrapper FindMeta: @@ -1013,7 +1011,7 @@ return nullptr; } -const Meta* ArgConverter::GetMeta(std::string name) { +const Meta* ArgConverter::GetMeta(const std::string& name) { bool found; const Meta* meta = Caches::Metadata->Get(name, found); if (meta != nullptr || found) { diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index 7aa2065b..6fbc0b10 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -2,6 +2,7 @@ #define Caches_h #include +#include #include #include "Common.h" @@ -16,6 +17,23 @@ struct ObjectWeakCallbackState; class PromiseRejectionTracker; class IsolateTracked; +// Declaring both halves transparent lets robin_hood probe a map keyed by +// std::string with a string_view, so callers holding a const char* from the +// Obj-C runtime do not have to allocate one just to look up. +struct TransparentStringHash { + using is_transparent = void; + size_t operator()(std::string_view key) const { + return robin_hood::hash_bytes(key.data(), key.size()); + } +}; + +struct TransparentStringEqual { + using is_transparent = void; + bool operator()(std::string_view lhs, std::string_view rhs) const { + return lhs == rhs; + } +}; + struct pair_hash { template std::size_t operator()(const std::pair& pair) const { @@ -101,7 +119,8 @@ class Caches { std::unique_ptr>> Prototypes; robin_hood::unordered_map>> + std::unique_ptr>, + TransparentStringHash, TransparentStringEqual> ClassPrototypes; robin_hood::unordered_map< const BaseClassMeta*, diff --git a/NativeScript/runtime/ConcurrentMap.h b/NativeScript/runtime/ConcurrentMap.h index 91fb9842..353cb687 100644 --- a/NativeScript/runtime/ConcurrentMap.h +++ b/NativeScript/runtime/ConcurrentMap.h @@ -9,36 +9,36 @@ namespace tns { template class ConcurrentMap { public: - inline void Insert(TKey& key, TValue value) { - std::lock_guard writerLock(this->containerMutex_); - this->container_[key] = value; - } - - inline TValue Get(TKey& key) { - bool found; - return this->Get(key, found); - } - - inline TValue Get(TKey& key, bool& found) { - std::lock_guard writerLock(this->containerMutex_); - auto it = this->container_.find(key); - found = it != this->container_.end(); - if (found) { - return it->second; - } - return nullptr; - } - - inline bool ContainsKey(TKey& key) { - std::lock_guard writerLock(this->containerMutex_); - auto it = this->container_.find(key); - return it != this->container_.end(); - } - - inline void Remove(TKey& key) { - std::lock_guard writerLock(this->containerMutex_); - this->container_.erase(key); - } + inline void Insert(const TKey& key, TValue value) { + std::lock_guard writerLock(this->containerMutex_); + this->container_[key] = value; + } + + inline TValue Get(const TKey& key) { + bool found; + return this->Get(key, found); + } + + inline TValue Get(const TKey& key, bool& found) { + std::lock_guard writerLock(this->containerMutex_); + auto it = this->container_.find(key); + found = it != this->container_.end(); + if (found) { + return it->second; + } + return nullptr; + } + + inline bool ContainsKey(const TKey& key) { + std::lock_guard writerLock(this->containerMutex_); + auto it = this->container_.find(key); + return it != this->container_.end(); + } + + inline void Remove(const TKey& key) { + std::lock_guard writerLock(this->containerMutex_); + this->container_.erase(key); + } inline void ForEach(const std::function& func) { std::lock_guard writerLock(this->containerMutex_); diff --git a/NativeScript/runtime/Helpers.h b/NativeScript/runtime/Helpers.h index 7611e038..c1ba13c6 100644 --- a/NativeScript/runtime/Helpers.h +++ b/NativeScript/runtime/Helpers.h @@ -41,59 +41,134 @@ inline v8::Local ToV8String(v8::Isolate* isolate, const char* value, return v8::String::NewFromUtf8(isolate, value, v8::NewStringType::kNormal, length) .ToLocalChecked(); } + +// Without this overload a bare `const char*` — every string literal, every +// c_str(), every jsName() — picks the std::string one and pays for a temporary +// just to reach V8. +inline v8::Local ToV8String(v8::Isolate* isolate, const char* value) { + return v8::String::NewFromUtf8(isolate, value).ToLocalChecked(); +} #ifdef __OBJC__ +// Both sides store text as either 8-bit or UTF-16, never UTF-8, so the buffer is +// handed to V8 in whichever width CFString already holds. Going through +// -UTF8String instead would encode the string twice (once for the bytes, once +// for -lengthOfBytesUsingEncoding:), allocate, and return nil for strings +// containing a lone surrogate — silently turning them into "". inline v8::Local ToV8String(v8::Isolate* isolate, const NSString* value) { - /* - // TODO: profile if this is faster - // maybe have multiple conversion - if([value fastestEncoding] == NSUTF16StringEncoding) { - uint16_t static_buffer[256]; - uint16_t* targetBuffer = static_buffer; - bool isDynamic = false; - auto length = [value - maximumLengthOfBytesUsingEncoding:NSUTF16StringEncoding]; auto numberOfBytes = - length * sizeof(uint16_t); if (length > 256) { targetBuffer = - (uint16_t*)malloc(numberOfBytes); isDynamic = true; - } - NSUInteger usedLength = 0; - NSRange range = NSMakeRange(0, [value length]); - [value getBytes:targetBuffer maxLength:numberOfBytes - usedLength:&usedLength encoding:NSUTF16StringEncoding options:0 range:range - remainingRange:NULL]; - - auto result = v8::String::NewFromTwoByte(isolate, targetBuffer, - v8::NewStringType::kNormal, (int)[value length]).ToLocalChecked(); if - (isDynamic) { free(targetBuffer); - } - return result; + if (value == nil) { + return v8::String::Empty(isolate); } - */ - return v8::String::NewFromUtf8(isolate, [value UTF8String], v8::NewStringType::kNormal, - (int)[value lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) + + CFStringRef str = (__bridge CFStringRef)value; + CFIndex length = CFStringGetLength(str); + if (length == 0) { + return v8::String::Empty(isolate); + } + + // An ASCII pointer is handed back only when every code unit is < 0x80, so the + // code unit count doubles as the byte count. + if (const char* ascii = CFStringGetCStringPtr(str, kCFStringEncodingASCII)) { + return v8::String::NewFromOneByte(isolate, reinterpret_cast(ascii), + v8::NewStringType::kNormal, (int)length) + .ToLocalChecked(); + } + + if (const UniChar* utf16 = CFStringGetCharactersPtr(str)) { + return v8::String::NewFromTwoByte(isolate, reinterpret_cast(utf16), + v8::NewStringType::kNormal, (int)length) + .ToLocalChecked(); + } + + // Tagged pointers and some bridged strings expose neither buffer, so the + // contents have to be copied out. The narrow attempt writes at most `length` + // bytes, which always fits the UTF-16-sized buffer. + constexpr CFIndex kStackUnits = 256; + uint16_t stackBuffer[kStackUnits]; + std::vector heapBuffer; + uint16_t* buffer = stackBuffer; + if (length > kStackUnits) { + heapBuffer.resize((size_t)length); + buffer = heapBuffer.data(); + } + + CFRange range = CFRangeMake(0, length); + CFIndex usedLength = 0; + if (CFStringGetBytes(str, range, kCFStringEncodingASCII, 0, false, + reinterpret_cast(buffer), length, &usedLength) == length) { + return v8::String::NewFromOneByte(isolate, reinterpret_cast(buffer), + v8::NewStringType::kNormal, (int)length) + .ToLocalChecked(); + } + + CFStringGetCharacters(str, range, reinterpret_cast(buffer)); + return v8::String::NewFromTwoByte(isolate, buffer, v8::NewStringType::kNormal, (int)length) .ToLocalChecked(); } #endif -inline std::string ToString(v8::Isolate* isolate, const v8::Local& value) { +// Unwraps a value to the v8::String the text conversions below read from. +// A throwing toString() is swallowed rather than left pending, which is the +// contract v8::String::Utf8Value offered and callers were written against. +inline bool ToStringLocal(v8::Isolate* isolate, const v8::Local& value, + v8::Local& out) { if (value.IsEmpty()) { - return std::string(); + return false; + } + + if (value->IsString()) { + out = value.As(); + return true; } if (value->IsStringObject()) { - v8::Local obj = value.As()->ValueOf(); - return tns::ToString(isolate, obj); + out = value.As()->ValueOf(); + return true; } - v8::String::Utf8Value result(isolate, value); + v8::TryCatch tc(isolate); + return value->ToString(isolate->GetCurrentContext()).ToLocal(&out); +} - const char* val = *result; - if (val == nullptr) { +inline std::string ToString(v8::Isolate* isolate, const v8::Local& value) { + v8::Local str; + if (!ToStringLocal(isolate, value, str)) { return std::string(); } - return std::string(*result, result.length()); + { + v8::String::ValueView view(isolate, str); + if (view.is_one_byte()) { + const uint8_t* data = view.data8(); + uint32_t length = view.length(); + uint32_t i = 0; + while (i < length && data[i] < 0x80) { + i++; + } + // Pure ASCII already is its own UTF-8 encoding, so it can be copied + // straight out. A one-byte string with a high byte is Latin-1 and still + // needs widening, which the encode below handles. + if (i == length) { + return std::string(reinterpret_cast(data), length); + } + } + } + + size_t length = str->Utf8LengthV2(isolate); + std::string result(length, '\0'); + if (length > 0) { + str->WriteUtf8V2(isolate, result.data(), length, v8::String::WriteFlags::kReplaceInvalidUtf8); + } + + return result; } #ifdef __OBJC__ +// Encodes via V8 rather than -UTF8String, which returns nil for a string holding +// a lone surrogate — leaving callers to construct a std::string from nullptr. +// The unpaired half becomes U+FFFD, since it has no UTF-8 spelling. +inline std::string ToString(v8::Isolate* isolate, const NSString* value) { + return tns::ToString(isolate, tns::ToV8String(isolate, value)); +} + inline NSString* ToNSString(const std::string& v) { return [[[NSString alloc] initWithBytes:v.c_str() length:v.length() encoding:NSUTF8StringEncoding] S_AUTORELEASE]; @@ -164,8 +239,6 @@ inline bool ToBool(const v8::Local& value) { return result; } -std::vector ToVector(const std::string& value); - bool Exists(const char* fullPath); v8::Local ReadModule(v8::Isolate* isolate, const std::string& filePath); const char* ReadText(const std::string& filePath, long& length, bool& isNew); diff --git a/NativeScript/runtime/Helpers.mm b/NativeScript/runtime/Helpers.mm index 270f352f..d5a1cf0b 100644 --- a/NativeScript/runtime/Helpers.mm +++ b/NativeScript/runtime/Helpers.mm @@ -8,9 +8,7 @@ #include #include #include -#include #include -#include #include #include "Caches.h" #include "ErrorEvents.h" @@ -54,18 +52,6 @@ return std::u16string((const char16_t*)result.data16(), result.length()); } -std::vector tns::ToVector(const std::string& value) { -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // FIXME: std::codecvt_utf8_utf16 is deprecated - std::wstring_convert, char16_t> convert; - std::u16string value16 = convert.from_bytes(value); - - const uint16_t* begin = reinterpret_cast(value16.data()); - const uint16_t* end = reinterpret_cast(value16.data() + value16.size()); - std::vector vector(begin, end); - return vector; -} - bool tns::Exists(const char* fullPath) { struct stat statbuf; mode_t mode = S_IFDIR | S_IFREG; diff --git a/NativeScript/runtime/InlineFunctions.cpp b/NativeScript/runtime/InlineFunctions.cpp index abce884f..37682cdd 100644 --- a/NativeScript/runtime/InlineFunctions.cpp +++ b/NativeScript/runtime/InlineFunctions.cpp @@ -17,7 +17,7 @@ void InlineFunctions::Init(Local context) { } } -bool InlineFunctions::IsGlobalFunction(std::string name) { +bool InlineFunctions::IsGlobalFunction(const std::string& name) { return name == "CGPointMake" || name == "CGRectMake" || name == "CGSizeMake" || name == "UIEdgeInsetsMake" || name == "NSMakeRange" || name == "__decorate" || name == "__param" || diff --git a/NativeScript/runtime/InlineFunctions.h b/NativeScript/runtime/InlineFunctions.h index 87259e97..5255b13c 100644 --- a/NativeScript/runtime/InlineFunctions.h +++ b/NativeScript/runtime/InlineFunctions.h @@ -8,7 +8,7 @@ namespace tns { class InlineFunctions { public: static void Init(v8::Local context); - static bool IsGlobalFunction(std::string name); + static bool IsGlobalFunction(const std::string& name); }; } diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index fb29bbf2..0edf81ed 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -262,9 +262,7 @@ inline bool isBool() { } else if (argHelper.isString() && typeEncoding->type == BinaryTypeEncodingType::SelectorEncoding) { std::string str = tns::ToString(isolate, arg); - NSString* selStr = [NSString stringWithUTF8String:str.c_str()]; - SEL selector = NSSelectorFromString(selStr); - Interop::SetValue(dest, selector); + Interop::SetValue(dest, sel_registerName(str.c_str())); } else if (typeEncoding->type == BinaryTypeEncodingType::CStringEncoding) { if (arg->IsString()) { const char* value = nullptr; @@ -314,12 +312,21 @@ inline bool isBool() { } } else if (argHelper.isString() && typeEncoding->type == BinaryTypeEncodingType::UnicharEncoding) { - v8::String::Utf8Value utf8Value(isolate, arg); - std::vector vector = tns::ToVector(*utf8Value); - if (vector.size() > 1) { + Local str; + uint32_t length = 0; + unichar c = 0; + if (tns::ToStringLocal(isolate, arg, str)) { + // Scoped so the view's no-GC window closes before anything can throw. + v8::String::ValueView view(isolate, str); + length = view.length(); + if (length == 1) { + c = view.is_one_byte() ? view.data8()[0] : view.data16()[0]; + } + } + + if (length > 1) { throw NativeScriptException("Only one character string can be converted to unichar."); } - unichar c = (vector.size() == 0) ? 0 : vector[0]; Interop::SetValue(dest, c); } else if (argHelper.isString() && (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference || @@ -980,8 +987,7 @@ inline bool isBool() { return Null(isolate); } - NSString* selStr = NSStringFromSelector(result); - return tns::ToV8String(isolate, [selStr UTF8String]); + return tns::ToV8String(isolate, sel_getName(result)); } if (typeEncoding->type == BinaryTypeEncodingType::ProtocolEncoding) { @@ -1355,18 +1361,10 @@ inline bool isBool() { } if (type == BinaryTypeEncodingType::UnicharEncoding) { - unichar result = call->GetResult(); - char chars[2]; - - if (result > 127) { - chars[0] = (result >> 8) & (1 << 8) - 1; - chars[1] = result & (1 << 8) - 1; - } else { - chars[0] = result; - chars[1] = 0; - } - - return tns::ToV8String(isolate, chars); + // A unichar is a single UTF-16 code unit, so it goes over as one. + uint16_t result = call->GetResult(); + return v8::String::NewFromTwoByte(isolate, &result, v8::NewStringType::kNormal, 1) + .ToLocalChecked(); } if (type == BinaryTypeEncodingType::UCharEncoding) { @@ -1670,26 +1668,23 @@ inline bool isBool() { NSString* nsName = [e name]; NSString* nsReason = [e reason]; - std::string message = - nsReason ? [nsReason UTF8String] : (nsName ? [nsName UTF8String] : "NSException"); + Local messageV8 = tns::ToV8String(isolate, nsReason ?: (nsName ?: @"NSException")); + std::string message = tns::ToString(isolate, messageV8); - Local jsErrVal = Exception::Error(tns::ToV8String(isolate, message)); + Local jsErrVal = Exception::Error(messageV8); if (jsErrVal.IsEmpty() || !jsErrVal->IsObject()) { // Fallback: keep the description-only behavior if Error creation fails. - throw NativeScriptException([[e description] UTF8String]); + throw NativeScriptException(tns::ToString(isolate, [e description])); } Local jsErrObj = jsErrVal.As(); if (nsName != nil) { - jsErrObj - ->Set(context, tns::ToV8String(isolate, "name"), - tns::ToV8String(isolate, [nsName UTF8String])) + jsErrObj->Set(context, tns::ToV8String(isolate, "name"), tns::ToV8String(isolate, nsName)) .FromMaybe(false); } if (nsReason != nil) { jsErrObj - ->Set(context, tns::ToV8String(isolate, "message"), - tns::ToV8String(isolate, [nsReason UTF8String])) + ->Set(context, tns::ToV8String(isolate, "message"), tns::ToV8String(isolate, nsReason)) .FromMaybe(false); } @@ -1713,11 +1708,11 @@ inline bool isBool() { Local context = isolate->GetCurrentContext(); Local jsErrVal = - Exception::Error(tns::ToV8String(isolate, [[error localizedDescription] UTF8String])); + Exception::Error(tns::ToV8String(isolate, [error localizedDescription])); if (jsErrVal.IsEmpty() || !jsErrVal->IsObject()) { // Fallback: if for some reason we cannot create an Error object, throw a generic // NativeScriptException - throw NativeScriptException([[error localizedDescription] UTF8String]); + throw NativeScriptException(tns::ToString(isolate, [error localizedDescription])); } Local jsErrObj = jsErrVal.As(); @@ -1730,7 +1725,7 @@ inline bool isBool() { if (error.domain) { jsErrObj ->Set(context, tns::ToV8String(isolate, "domain"), - tns::ToV8String(isolate, [error.domain UTF8String])) + tns::ToV8String(isolate, error.domain)) .FromMaybe(false); } else { jsErrObj->Set(context, tns::ToV8String(isolate, "domain"), Null(isolate)).FromMaybe(false); diff --git a/NativeScript/runtime/MetadataBuilder.h b/NativeScript/runtime/MetadataBuilder.h index 36f275a6..c997d242 100644 --- a/NativeScript/runtime/MetadataBuilder.h +++ b/NativeScript/runtime/MetadataBuilder.h @@ -69,7 +69,7 @@ class MetadataBuilder { const MethodMeta* meta, v8::Local receiver, V8Args& args, - std::string containingClass, + const std::string& containingClass, bool isMethodCallback); static void RegisterAllocMethod( v8::Isolate* isolate, v8::Local ctorFuncTemplate, diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index 43d2a3eb..ce9f6417 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -482,8 +482,7 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta return; } - std::string description = [[target description] UTF8String]; - Local returnValue = tns::ToV8String(info.GetIsolate(), description); + Local returnValue = tns::ToV8String(info.GetIsolate(), [target description]); info.GetReturnValue().Set(returnValue); } @@ -764,21 +763,25 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta bool instanceMethod = info.This()->InternalFieldCount() > 0; V8FunctionCallbackArgs args(info); - std::string className = item->className_; + // Only the class-side call rewrites the name, so the common path reads + // item->className_ in place rather than copying it on every invocation. + const std::string* className = &item->className_; + std::string classWrapperName; Local thiz = info.This(); if (thiz->IsFunction()) { if (BaseDataWrapper* wrapper = tns::GetValue(isolate, thiz)) { ObjCClassWrapper* classWrapper = static_cast(wrapper); - className = class_getName(classWrapper->Klass()); + classWrapperName = class_getName(classWrapper->Klass()); + className = &classWrapperName; } } Local context = isolate->GetCurrentContext(); Local result = instanceMethod - ? MetadataBuilder::InvokeMethod(context, item->meta_, info.This(), args, className, true) - : MetadataBuilder::InvokeMethod(context, item->meta_, Local(), args, className, + ? MetadataBuilder::InvokeMethod(context, item->meta_, info.This(), args, *className, true) + : MetadataBuilder::InvokeMethod(context, item->meta_, Local(), args, *className, true); if (!result.IsEmpty()) { @@ -967,7 +970,8 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta Local MetadataBuilder::InvokeMethod(Local context, const MethodMeta* meta, Local receiver, V8Args& args, - std::string containingClass, bool isMethodCallback) { + const std::string& containingClass, + bool isMethodCallback) { Class klass = objc_getClass(containingClass.c_str()); // TODO: Find out if the isMethodCallback property can be determined based on a // UITableViewController.prototype.viewDidLoad.call(this) or super.viewDidLoad() call diff --git a/NativeScript/runtime/SymbolIterator.mm b/NativeScript/runtime/SymbolIterator.mm index 2e713b0a..15fdec02 100644 --- a/NativeScript/runtime/SymbolIterator.mm +++ b/NativeScript/runtime/SymbolIterator.mm @@ -86,7 +86,7 @@ if ([item isKindOfClass:[NSNumber class]]) { val = Number::New(isolate, [item doubleValue]); } else if ([item isKindOfClass:[NSString class]]) { - val = tns::ToV8String(isolate, [item UTF8String]); + val = tns::ToV8String(isolate, (NSString*)item); } else { auto wrapper = new ObjCDataWrapper(item); val = ArgConverter::CreateJsWrapper(context, wrapper, Local()); diff --git a/TestRunner/app/tests/Marshalling/NSStringTests.js b/TestRunner/app/tests/Marshalling/NSStringTests.js index 935b5441..da757e0d 100644 --- a/TestRunner/app/tests/Marshalling/NSStringTests.js +++ b/TestRunner/app/tests/Marshalling/NSStringTests.js @@ -39,6 +39,46 @@ describe(module.id, function () { expect(instance.valueForKey('x')).toBe(data); }); + function roundTrip(value) { + var JSObject = NSObject.extend({ + 'x': function () { + return this._x; + }, 'setX:': function (x) { + this._x = x; + } + }, { + exposedMethods: { + x: { returns: NSString }, + 'setX:': { returns: interop.types.void, params: [NSString] } + } + }); + + var instance = JSObject.alloc().init(); + instance.setValueForKey(value, 'x'); + return instance.valueForKey('x'); + } + + it("Marshals NSString with a lone surrogate", function () { + const data = 'a' + String.fromCharCode(0xD800) + 'b'; + expect(roundTrip(data)).toBe(data); + }); + + it("Marshals NSString across both storage widths", function () { + // ASCII, Latin-1 and non-BMP exercise the one-byte, two-byte and + // surrogate-pair paths respectively. + expect(roundTrip('plain ascii')).toBe('plain ascii'); + expect(roundTrip('café naïve')).toBe('café naïve'); + expect(roundTrip('你好世界')).toBe('你好世界'); + expect(roundTrip('emoji 👋🏽 here')).toBe('emoji 👋🏽 here'); + }); + + it("Marshals NSString longer than the conversion stack buffer", function () { + const ascii = 'a'.repeat(1000); + const wide = '𝄞'.repeat(1000); + expect(roundTrip(ascii)).toBe(ascii); + expect(roundTrip(wide)).toBe(wide); + }); + it("String", function () { var str = NSString.string(); expect(str.isKindOfClass(NSString)).toBe(true); diff --git a/TestRunner/app/tests/Marshalling/Primitives/Instance.js b/TestRunner/app/tests/Marshalling/Primitives/Instance.js index 47b4eace..42a5e671 100644 --- a/TestRunner/app/tests/Marshalling/Primitives/Instance.js +++ b/TestRunner/app/tests/Marshalling/Primitives/Instance.js @@ -231,6 +231,12 @@ describe(module.id, function () { // Both outcomes are acceptable depending on build configuration expect(threw === true || threw === false).toBe(true); }); + + it("InstanceMethodWithNonAsciiUnichar", function () { + var instance = TNSPrimitives.alloc().init(); + expect(instance.methodWithUnichar('é')).toBe('é'); + expect(instance.methodWithUnichar('✓')).toBe('✓'); + }); it("InstanceMethodWithNSNumber1", function () { var result = TNSPrimitives.alloc().init().methodWithNSNumber(0);