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());
}