Kotlin-native: Can't use NSString.initWithFormat

Created on 31 Jul 2018  Â·  14Comments  Â·  Source: JetBrains/kotlin-native

Hello!

I try to create formatted string in iOS platform module.
In Swift code should be:

NSString(format: "test %s", "test 2")

but in Kotlin/Native i can't use same init method.

public convenience init(format: NSString, _ args: CVarArg...)

was converted to

@kotlinx.cinterop.ObjCMethod public external fun platform.Foundation.NSString.initWithFormat(format: kotlin.String, arguments: platform.posix.va_list? /* = kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVar /* = kotlinx.cinterop.ByteVarOf<kotlin.Byte> */>? */): kotlin.String { /* compiled code */ }

and then call in kotlin was:

NSString().initWithFormat("test %s", "test 2")

what throw exception:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** initialization method -initWithFormat:locale:arguments: cannot be sent to an abstract object of class __NSCFConstantString: Create a concrete instance!'

how create NSString with format in kotlin/native?

interop

Most helpful comment

Yet another nontrivial workaround :)
@SvyatoslavScherbina, what do you think about creating a special ios-extension library? Maybe it shouldn't be exactly a library, just a set of tips and tricks, I don't know...
I mean, iOs is one of the growth drivers for Kotlin/Native. A lot of developers are writing various adapters and workarounds to support common code on iOS / Android platforms. It would be great to collect everything in one place, so that each developer does not do the same work again :)

All 14 comments

init* methods aren't supposed to be used directly in Kotlin/Native.
Initializers declared in Objective-C classes are available as constructors instead, and initializers declared in Objective-C categories -- as create factory methods.

However initWithFormat appears to be variadic, and Kotlin/Native doesn't support variadic Objective-C methods.

By the way, do you have a case that is not covered by pure Kotlin methods and string interpolation? If so, could you please provide more details about it?

i create multiplatform resource classes to use it in viewmodels (in common module).

in common module have expect class:

expect sealed class StringDesc {
    class Resource(stringRes: StringResource): StringDesc
    class ResourceFormatted(stringRes: StringResource, args: List<Any>): StringDesc {
        constructor(stringRes: StringResource, vararg args: Any)
    }
    class Plural(pluralsRes: PluralsResource, number: Int): StringDesc
    class PluralFormatted(pluralsRes: PluralsResource, number: Int, args: List<Any>): StringDesc {
        constructor(pluralsRes: PluralsResource, number: Int, vararg args: Any)
    }
    class Raw(string: String): StringDesc
    class Composition(args: List<StringDesc>): StringDesc
}

and for android:

actual sealed class StringDesc {
    actual data class Resource actual constructor(val stringRes: StringResource) : StringDesc() {
        override fun toString(context: Context): String {
            return context.getString(stringRes.resourceId)
        }
    }

    actual data class ResourceFormatted actual constructor(val stringRes: StringResource, val args: List<Any>) : StringDesc() {
        override fun toString(context: Context): String {
            return context.getString(stringRes.resourceId, *(args.toTypedArray()))
        }

        actual constructor(stringRes: StringResource, vararg args: Any) : this(stringRes, args.toList())
    }

    actual data class Plural actual constructor(val pluralsRes: PluralsResource, val number: Int) : StringDesc() {
        override fun toString(context: Context): String {
            return context.resources.getQuantityString(pluralsRes.resourceId, number)
        }
    }

    actual data class PluralFormatted actual constructor(val pluralsRes: PluralsResource, val number: Int, val args: List<Any>) : StringDesc() {
        override fun toString(context: Context): String {
            return context.resources.getQuantityString(pluralsRes.resourceId, number, *(args.toTypedArray()))
        }

        actual constructor(pluralsRes: PluralsResource, number: Int, vararg args: Any) : this(pluralsRes, number, args.toList())
    }

    actual data class Raw actual constructor(val string: String) : StringDesc() {
        override fun toString(context: Context): String {
            return string
        }
    }

    actual data class Composition actual constructor(val args: List<StringDesc>) : StringDesc() {
        override fun toString(context: Context): String {
            return StringBuilder().apply {
                args.forEach {
                    append(it.toString(context))
                }
            }.toString()
        }
    }

    abstract fun toString(context: Context): String
}

but for ios string formatting should be in native swift code for now...and now i get:

actual sealed class StringDesc {
    actual data class Resource actual constructor(val stringRes: StringResource) : StringDesc() {
        override fun toLocalizedString(formatter: Formatter): String {
            return NSBundle.mainBundle().localizedStringForKey(stringRes.resourceId, null, null)
        }
    }

    actual data class ResourceFormatted actual constructor(val stringRes: StringResource, val args: List<Any>) : StringDesc() {
        override fun toLocalizedString(formatter: Formatter): String {
            val string = NSBundle.mainBundle().localizedStringForKey(stringRes.resourceId, null, null)
            return formatter.formatString(string, args.toTypedArray())
        }

        actual constructor(stringRes: StringResource, vararg args: Any) : this(stringRes, args.toList())
    }

    actual data class Plural actual constructor(val pluralsRes: PluralsResource, val number: Int) : StringDesc() {
        override fun toLocalizedString(formatter: Formatter): String {
            return formatter.plural(pluralsRes.resourceId, number)
        }
    }

    actual data class PluralFormatted actual constructor(val pluralsRes: PluralsResource, val number: Int, val args: List<Any>) : StringDesc() {
        override fun toLocalizedString(formatter: Formatter): String {
            return formatter.formatPlural(pluralsRes.resourceId, number, args.toTypedArray())
        }

        actual constructor(pluralsRes: PluralsResource, number: Int, vararg args: Any) : this(pluralsRes, number, args.toList())
    }

    actual data class Raw actual constructor(val string: String) : StringDesc() {
        override fun toLocalizedString(formatter: Formatter): String {
            return string
        }
    }

    actual data class Composition actual constructor(val args: List<StringDesc>) : StringDesc() {
        override fun toLocalizedString(formatter: Formatter): String {
            return StringBuilder().apply {
                args.forEach {
                    append(it.toLocalizedString(formatter))
                }
            }.toString()
        }
    }

    abstract fun toLocalizedString(formatter: Formatter): String

    interface Formatter {
        fun formatString(string: String, args: Array<out Any>): String
        fun plural(key: String, number: Int): String
        fun formatPlural(key: String, number: Int, args: Array<out Any>): String
    }
}

it's work, but in swift for get string from resource you should pass Formatter implementation, like this:

class StringFormatter: NSObject, EIFStringDescFormatter {
  func formatString(string: String, args: EIFStdlibArray) -> String {
    var cargs = Array<CVarArg>()
    for i in 0...(args.size - 1) {
      guard let arg = args.get(index: i) as? CVarArg else {
        continue
      }

      cargs.append(arg)
    }
    return String(format: string, arguments: cargs)
  }

  func plural(key: String, number: Int32) -> String {
    let localized = NSLocalizedString(key, comment: "")
    return String.localizedStringWithFormat(localized, number)
  }

  func formatPlural(key: String, number: Int32, args: EIFStdlibArray) -> String {
    let localized = NSLocalizedString(key, comment: "")
    let pluralized = String.localizedStringWithFormat(localized, number)

    var cargs = Array<CVarArg>()
    for i in 0...(args.size - 1) {
      guard let arg = args.get(index: i) as? CVarArg else {
        continue
      }

      cargs.append(arg)
    }
    return String(format: pluralized, cargs)
  }
}

ideally it should be in common-ios module in kotlin. but i can't use String(format: args:) in kotlin.

resourceclasses is simple:
android:

actual class PluralsResource(@PluralsRes val resourceId: Int)
actual class StringResource(@StringRes val resourceId: Int)

ios:

actual class PluralsResource(val resourceId: String)
actual class StringResource(val resourceId: String)

Thank you for the detailed explanation!
Kotlin/Native supports variadic C functions, so as a workaround you can try to use CoreFoundation string formatting, but this appears to be somewhat tricky:

import kotlinx.cinterop.*
import platform.CoreFoundation.*
import platform.darwin.*
import platform.Foundation.*


fun format(format: String, vararg args: Any?): String {
    val cfString: CFStringRef = CFBridgingRetain(format)!!.reinterpret()
    try {
        val encodedArgs = args.map { 
            if (it is String) 
                // Prevent from being converted to C string when passing as C variadic argument:
                object : NSObject() {
                    override fun description(): String? = it
                }
            else it
        }

        val cfFormattedString = CFStringCreateWithFormat(null, null, cfString, *encodedArgs.toTypedArray())
        return CFBridgingRelease(cfFormattedString) as String
    } finally {
        CFRelease(cfString)
    }
}

For example:

fun main(args: Array<String>) {
    println(format("%@ %@ %d", "один", "two", 3))
}

good, thx.
it can help in ResourceFormatted case, but for Plural / PluralFormatted should be used String.localizedStringWithFormat (plurals in ios - https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LocalizingYourApp/LocalizingYourApp.html#//apple_ref/doc/uid/10000171i-CH5-SW10 ).

Yes, you are right.
localizedStringWithFormat can be workarounded by writing a non-variadic wrapper in Objective-C. And you can easily add Objective-C code to your project within a .def file, e.g. formatting.def:

language = Objective-C
---
#import <Foundation/NSString.h>

NSString* NSLocalizedStringWithFormat(NSString* format, int number) {
    return [NSString localizedStringWithFormat:format, number]; 
}

It can be added to your Gradle build using interop artifact. Something like this should work:

konanArtifacts {
    interop('formatting')
    yourProgram('program') {
        libraries {
            artifact 'foo'
        }    
    }
}

(see documentation for more details).

After that NSLocalizedStringWithFormat can be imported to your program from formatting package.

Yet another nontrivial workaround :)
@SvyatoslavScherbina, what do you think about creating a special ios-extension library? Maybe it shouldn't be exactly a library, just a set of tips and tricks, I don't know...
I mean, iOs is one of the growth drivers for Kotlin/Native. A lot of developers are writing various adapters and workarounds to support common code on iOS / Android platforms. It would be great to collect everything in one place, so that each developer does not do the same work again :)

Looks like a good idea, maybe on our side we shall support variadic functions eventually, and in the meantime keep docs like this issue an information source. @subprogram if you could consider creating such adapters library, it would be valuable contribution for us.

I'm afraid I have no time for this... If someone create project and write a basis of such library, I'll make a few pull requests.

Thank you for the detailed explanation!
Kotlin/Native supports variadic C functions, so as a workaround you can try to use CoreFoundation string formatting, but this appears to be somewhat tricky:

import kotlinx.cinterop.*
import platform.CoreFoundation.*
import platform.darwin.*
import platform.Foundation.*


fun format(format: String, vararg args: Any?): String {
    val cfString: CFStringRef = CFBridgingRetain(format)!!.reinterpret()
    try {
        val encodedArgs = args.map { 
            if (it is String) 
                // Prevent from being converted to C string when passing as C variadic argument:
                object : NSObject() {
                    override fun description(): String? = it
                }
            else it
        }

        val cfFormattedString = CFStringCreateWithFormat(null, null, cfString, *encodedArgs.toTypedArray())
        return CFBridgingRelease(cfFormattedString) as String
    } finally {
        CFRelease(cfString)
    }
}

For example:

fun main(args: Array<String>) {
    println(format("%@ %@ %d", "один", "two", 3))
}

@SvyatoslavScherbina how convert to UTF-8? I use your code but return !UTF-8 :so sad:

@lehanphong
Consider using stringFromUtf8OrThrow/stringFromUtf8 or casting to NSString and using one of its encoding methods.

Thank you for the detailed explanation!
Kotlin/Native supports variadic C functions, so as a workaround you can try to use CoreFoundation string formatting, but this appears to be somewhat tricky:

import kotlinx.cinterop.*
import platform.CoreFoundation.*
import platform.darwin.*
import platform.Foundation.*


fun format(format: String, vararg args: Any?): String {
    val cfString: CFStringRef = CFBridgingRetain(format)!!.reinterpret()
    try {
        val encodedArgs = args.map { 
            if (it is String) 
                // Prevent from being converted to C string when passing as C variadic argument:
                object : NSObject() {
                    override fun description(): String? = it
                }
            else it
        }

        val cfFormattedString = CFStringCreateWithFormat(null, null, cfString, *encodedArgs.toTypedArray())
        return CFBridgingRelease(cfFormattedString) as String
    } finally {
        CFRelease(cfString)
    }
}

For example:

fun main(args: Array<String>) {
    println(format("%@ %@ %d", "один", "two", 3))
}

@SvyatoslavScherbina Your workaround no longer works in Kotlin 1.3.30-eap-125. When exporting the code as a framework I get the following error:

e: /Users/thomas/[...]/Strings.kt: (34, 81): spread operator is not supported for variadic C functions

I'm not sure if this is a bug or if this is supposed to no longer work anymore.

Spread operator is not supposed to work for C variadic functions anymore.

To workaround the original issue starting with 1.3.30, you can add cinterop with the following .def file:

language = Objective-C
---
#import <Foundation/NSString.h>

NSString* format(NSString* format, ...) {
    va_list args;
    va_start(args, format);
    NSString* result = [[NSString alloc] initWithFormat:format arguments:args];
    va_end(args);
    return result;
}

Note that Kotlin String passed as variadic argument is still treated as C string, so it still has to be hidden. But the conversion is now based on static type, so upcasting to Any works properly:

val String.va: Any get() = this

fun main() {
    println(format("%@ %@ %d", "один".va, "two".va, 3))
}

Starting from 1.3.40, it will be possible to use initWithFormat directly:

NSString.create(format = "%@ %@ %d", args = *arrayOf("один" as NSString, "two" as NSString, 3))

Casting to NSString is required to resolve ambiguity between passing Kotlin string as C string and as NSString (casting to Any works too).

Named arguments are required to resolve ambiguity between initWithFormat: and initWithFormat:locale:.

Alternatively one could use stringWithFormat: class function instead:

NSString.stringWithFormat("%@ %@ %d", "один" as NSString, "two" as NSString, 3)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

alastaircoote picture alastaircoote  Â·  3Comments

nvlizlo picture nvlizlo  Â·  4Comments

AregevDev picture AregevDev  Â·  3Comments

jonnyzzz picture jonnyzzz  Â·  4Comments

msink picture msink  Â·  4Comments