diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index 523e2652e60..34b5dadce2d 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -3565,6 +3565,11 @@ interopTest("interop_objc_smoke") { if (project.target instanceof KonanTarget.IOS_X64) { UtilsKt.codesign(project, "$buildDir/libobjcsmoke.dylib") } + + copy { + from("interop/objc/Localizable.stringsdict") + into("$testOutputLocal/$name/$target/en.lproj/") + } } } diff --git a/backend.native/tests/framework/values/values.swift b/backend.native/tests/framework/values/values.swift index 3bd61ac6ad2..a9ec68546be 100644 --- a/backend.native/tests/framework/values/values.swift +++ b/backend.native/tests/framework/values/values.swift @@ -1078,16 +1078,41 @@ func testGH3525() throws { } func testStringConversion() throws { - let test = TestStringConversion() + func test1() throws { + let test = TestStringConversion() - let buffer = NSMutableString() - buffer.append("a") - test.str = buffer - buffer.append("b") + let buffer = NSMutableString() + buffer.append("a") + test.str = buffer + buffer.append("b") - try assertEquals(actual: buffer, expected: "ab") - // Ensure test.str isn't affected by buffer mutation: - try assertEquals(actual: test.str as! NSString, expected: "a") + try assertEquals(actual: buffer, expected: "ab") + // Ensure test.str isn't affected by buffer mutation: + try assertEquals(actual: test.str as! NSString, expected: "a") + } + + func ensureNoCopy(nsStr: NSString) throws { + let test = TestStringConversion() + + test.str = nsStr + let nsStr2 = test.str as! NSString + + // Ensure no additional NSString created on both conversions: + try assertTrue(nsStr === nsStr2) + } + + func test2() throws { + var str = "a" + str += NSObject().description + try ensureNoCopy(nsStr: str as NSString) + + try ensureNoCopy(nsStr: NSString("abc")) + + try ensureNoCopy(nsStr: NSString(format: "%d%d%d", 3, 2, 1)) + } + + try test1() + try test2() } // -------- Execution of the test -------- diff --git a/backend.native/tests/interop/objc/Localizable.stringsdict b/backend.native/tests/interop/objc/Localizable.stringsdict new file mode 100644 index 00000000000..1f284ab9b84 --- /dev/null +++ b/backend.native/tests/interop/objc/Localizable.stringsdict @@ -0,0 +1,27 @@ + + + + + + + + screen_main_plural_string + + NSStringLocalizedFormatKey + %#@value@ + value + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + Plural: %d apples + one + Plural: one apple + zero + Plural: no apples + + + + diff --git a/backend.native/tests/interop/objc/objcSmoke.def b/backend.native/tests/interop/objc/objcSmoke.def index 7dfc9ef78bc..577fdcbbac2 100644 --- a/backend.native/tests/interop/objc/objcSmoke.def +++ b/backend.native/tests/interop/objc/objcSmoke.def @@ -1,4 +1,4 @@ language = Objective-C -headers = Foundation/NSArray.h Foundation/NSValue.h Foundation/NSString.h Foundation/NSStream.h +headers = Foundation/NSArray.h Foundation/NSValue.h Foundation/NSString.h Foundation/NSStream.h Foundation/NSBundle.h headerFilter = **/smoke.h Foundation/NSArray.h Foundation/NSValue.h Foundation/NSString.h Foundation/NSStream.h \ - objc/objc.h + objc/objc.h Foundation/NSBundle.h diff --git a/backend.native/tests/interop/objc/smoke.h b/backend.native/tests/interop/objc/smoke.h index b61dda691ed..cfbf7116050 100644 --- a/backend.native/tests/interop/objc/smoke.h +++ b/backend.native/tests/interop/objc/smoke.h @@ -224,3 +224,18 @@ Protocol* getPrinterProtocol() { @interface TestAllocNoRetain : NSObject @property BOOL ok; @end; + +@interface CustomString : NSString +- (instancetype)initWithValue:(int)value; +@property int value; +@end; + +CustomString* _Nonnull createCustomString(int value) { + return [[CustomString alloc] initWithValue:value]; +} + +int getCustomStringValue(CustomString* str) { + return str.value; +} + +extern BOOL customStringDeallocated; diff --git a/backend.native/tests/interop/objc/smoke.kt b/backend.native/tests/interop/objc/smoke.kt index ed864770205..e86fecd5e9b 100644 --- a/backend.native/tests/interop/objc/smoke.kt +++ b/backend.native/tests/interop/objc/smoke.kt @@ -29,6 +29,8 @@ fun run() { testAllocNoRetain() testNSOutputStreamToMemoryConstructor() testExportObjCClass() + testCustomString() + testLocalizedStrings() assertEquals(2, ForwardDeclaredEnum.TWO.value) @@ -457,6 +459,28 @@ fun testExportObjCClass() { xor (TestExportObjCClass4().objCClassName == TestExportObjCClass34Name)) } +fun testCustomString() { + assertFalse(customStringDeallocated) + + fun test() = autoreleasepool { + val str: String = createCustomString(321) + assertEquals("321", str) + assertEquals("CustomString", str.objCClassName) + assertEquals(321, getCustomStringValue(str)) + } + + test() + kotlin.native.internal.GC.collect() + assertTrue(customStringDeallocated) +} + +fun testLocalizedStrings() { + val key = "screen_main_plural_string" + val localizedString = NSBundle.mainBundle.localizedStringForKey(key, value = "", table = "Localizable") + val string = NSString.localizedStringWithFormat(localizedString, 5) + assertEquals("Plural: 5 apples", string) +} + private val Any.objCClassName: String get() = object_getClassName(this)!!.toKString() diff --git a/backend.native/tests/interop/objc/smoke.m b/backend.native/tests/interop/objc/smoke.m index d41b64d97a9..3b708c141f7 100644 --- a/backend.native/tests/interop/objc/smoke.m +++ b/backend.native/tests/interop/objc/smoke.m @@ -289,3 +289,34 @@ static CustomRetainMethodsImpl* retainedCustomRetainMethodsImpl; return self; } @end; + +BOOL customStringDeallocated = NO; + +@implementation CustomString { + NSString* delegate; +} + +- (instancetype)initWithValue:(int)value { + if (self = [super init]) { + self->delegate = @(value).description; + self.value = value; + } + return self; +} + +- (unichar)characterAtIndex:(NSUInteger)index { + return [self->delegate characterAtIndex:index]; +} + +- (NSUInteger)length { + return self->delegate.length; +} + +- (id)copyWithZone:(NSZone *)zone { + return self; +} + +- (void)dealloc { + customStringDeallocated = YES; +} +@end; diff --git a/runtime/src/main/cpp/ObjCInteropUtils.mm b/runtime/src/main/cpp/ObjCInteropUtils.mm index e1eeaf86951..4ff29d73c1d 100644 --- a/runtime/src/main/cpp/ObjCInteropUtils.mm +++ b/runtime/src/main/cpp/ObjCInteropUtils.mm @@ -19,6 +19,7 @@ #if KONAN_OBJC_INTEROP #import +#import #import #import #import "Memory.h" @@ -69,12 +70,16 @@ OBJ_GETTER(Kotlin_Interop_CreateKStringFromNSString, NSString* str) { RETURN_OBJ(nullptr); } - size_t length = [str length]; - NSRange range = {0, length}; + CFStringRef immutableCopyOrSameStr = CFStringCreateCopy(nullptr, (CFStringRef)str); + + auto length = CFStringGetLength(immutableCopyOrSameStr); + CFRange range = {0, length}; ArrayHeader* result = AllocArrayInstance(theStringTypeInfo, length, OBJ_RESULT)->array(); KChar* rawResult = CharArrayAddressOfElementAt(result, 0); - [str getCharacters:rawResult range:range]; + CFStringGetCharacters(immutableCopyOrSameStr, range, rawResult); + + result->obj()->meta_object()->associatedObject_ = (void*)immutableCopyOrSameStr; RETURN_OBJ(result->obj()); }