Why can't I pass an optional Swift String to C

2020-03-30 06:59发布

I have a C function that deals with C strings. The function actually allows strings to be NULL pointers. The declaration is like follows:

size_t embeddedSize ( const char *_Nullable string );

It is no problem to use this function like this in C:

size_t s1 = embeddedSize("Test");
size_t s2 = embeddedSize(NULL); // s2 will be 0

Now I'm trying to use it from Swift. The following code works

let s1 = embeddedSize("Test")
let s2 = embeddedSize(nil) // s2 will be 0

But what doesn't work is passing an optional string to it! This code will not compile:

let someString: String? = "Some String"
let s2 = embeddedSize(someString)

The compiler throws an error about the optional not being unwrapped and Xcode asks me if I maybe forgot to add ! or ?. However, why would I want to unwrap it? NULL or nil are valid values to be fed to the function. See above, I just passed nil to it directly and that compiled just well and returned the expected result. In my real code the string is fed from external and it is optional, I cannot force unwrap it, that will break if the string was nil. So how can I call that function with an optional string?

3条回答
够拽才男人
2楼-- · 2020-03-30 07:18

In Swift 2, the C function

size_t embeddedSize ( const char *_Nullable string );

is mapped to Swift as

func embeddedSize(string: UnsafePointer<Int8>) -> Int

and you can pass a (non-optional) Swift string as the argument, as documented in Interacting with C APIs in the "Using Swift with Cocoa and Objective-C" reference:

Constant Pointers

When a function is declared as taking a UnsafePointer<Type> argument, it can accept any of the following:

  • ...
  • A String value, if Type is Int8 or UInt8. The string will automatically be converted to UTF8 in a buffer, and a pointer to that buffer is passed to the function.
  • ...

You can also pass nil because in Swift 2, nil is an allowed value for UnsafePointer.

As @zneak pointed out, the "automatic conversion" to UTF-8 does not work for optional strings in Swift 2, so you have to (conditionally) unwrap the string:

let someString: String? = "Some String"
let s2: size_t
if let str = someString {
    s2 = embeddedSize(str)
} else {
    s2 = embeddedSize(nil)
}

Using the map method of Optional and the nil-coalescing operator ??, this can be written more compactly as

let someString: String? = "Some String"
let s2 = someString.map { embeddedSize($0) } ?? embeddedSize(nil)

One generic solution was suggested by @zneak.

Here is another possible solution. String has a method

func withCString<Result>(@noescape f: UnsafePointer<Int8> throws -> Result) rethrows -> Result

which calls the closure with a pointer to the UTF-8 representation of the string, life-extended through the execution of f. So for a non-optional string, the following two statements are equivalent:

let s1 = embeddedSize("Test")
let s1 = "Test".withCString { embeddedSize($0) }

We can define a similar method for optional strings. Since extensions of generic types can restrict the type placeholder only to protocols and not to concrete types, we have to define a protocol that String conforms to:

protocol CStringConvertible {
    func withCString<Result>(@noescape f: UnsafePointer<Int8> throws -> Result) rethrows -> Result
}

extension String: CStringConvertible { }

extension Optional where Wrapped: CStringConvertible {
    func withOptionalCString<Result>(@noescape f: UnsafePointer<Int8> -> Result) -> Result {
        if let string = self {
            return string.withCString(f)
        } else {
            return f(nil)
        }
    }
}

Now the above C function can be called with the optional string argument as

let someString: String? = "Some String"
let s2 = someString.withOptionalCString { embeddedSize($0) }

For multiple C string arguments, the closure can be nested:

let string1: String? = "Hello"
let string2: String? = "World"

let result = string1.withOptionalCString { s1 in
    string2.withOptionalCString { s2 in
        calculateTotalLength(s1, s2)
    }
}

Apparently, the problem has been solved in Swift 3. Here the C function is mapped to

func embeddedSize(_ string: UnsafePointer<Int8>?) -> Int

and passing a String? compiles and works as expected, both for nil and non-nil arguments.

查看更多
Bombasti
3楼-- · 2020-03-30 07:19

All the solutions that indirect the call by extra swift level, work fine if you only have one parameter. But I also have C functions like this (strX are not the real parameter names, the call is actually simplified):

size_t calculateTotalLength (
    const char *_Nullable str1,
    const char *_Nullable str2,
    const char *_Nullable str3,
    const char *_Nullable str4,
    const char *_Nullable str5
);

And here this indirection becomes impractical, as I need one indirection per argument, 5 indirections for the function above.

Here's the best (ugly) "hack" I came up with so far, that avoids this problem (I'm still happy to see any better solutions, maybe someone gets an idea seeing that code):

private
func SwiftStringToData ( string: String? ) -> NSData? {
    guard let str = string else { return nil }
    return str.dataUsingEncoding(NSUTF8StringEncoding)
}

let str1 = SwiftStringToData(string1)
let str2 = SwiftStringToData(string2)
let str3 = SwiftStringToData(string3)
let str4 = SwiftStringToData(string4)
let str5 = SwiftStringToData(string5)

let totalLength = calculateTotalLength(
    str1 == nil ? 
        UnsafePointer<Int8>(nil) : UnsafePointer<Int8>(str1!.bytes),
    str2 == nil ? 
        UnsafePointer<Int8>(nil) : UnsafePointer<Int8>(str2!.bytes),
    str3 == nil ? 
        UnsafePointer<Int8>(nil) : UnsafePointer<Int8>(str3!.bytes),
    str4 == nil ? 
        UnsafePointer<Int8>(nil) : UnsafePointer<Int8>(str4!.bytes),
    str5 == nil ? 
        UnsafePointer<Int8>(nil) : UnsafePointer<Int8>(str5!.bytes),
)

In case someone thinks about just passing the result of data.bytes back to the caller, this is a very bad idea! The pointer returned by data.bytes is only guaranteed to stay valid as long as data itself stays alive and ARC will kill data as soon as it can. So the following is not valid code:

// --- !!! BAD CODE, DO NOT USE !!! ---
private
func SwiftStringToData ( string: String? ) -> UnsafePointer<Int8>? {
    guard let str = string else { UnsafePointer<Int8>(nil) }
    let data = str.dataUsingEncoding(NSUTF8StringEncoding)
    return UnsafePointer<Int8>(data.bytes)
}

There is no guarantee that data is still alive when this method returns, the returned pointer might be a dangling pointer! Then I thought about the following:

// --- !!! BAD CODE, DO NOT USE !!! ---
private
func DataToCString ( data: NSData? ) -> UnsafePointer<Int8>? {
    guard let d = data else { UnsafePointer<Int8>(nil) }
    return UnsafePointer<Int8>(d.bytes)
}

let str1 = SwiftStringToData(string1)
let cstr1 = DataToCString(str1)
// (*1)
// ....
let totalLength = calculateTotalLength(cstr1, /* ... */)

But that is not guaranteed to be safe either. The compiler sees that str1 is not referenced anymore when it gets to (*1), so it may not keep it alive and when we reach the last line, cstr1 is already a dangling pointer.

It is only safe as shown in my first sample, since there the NSData objects (str1 etc.) must be kept alive to the calculateTotalLength() function call and some methods (like bytes of NSData or UTF8String of NSString) are tagged to return an internal pointer, in which case the compiler will make sure the lifetime of the object is extended in the current scope as long as the object or such an internal pointer is still referenced. This mechanism makes sure, that the returned pointers (str1.bytes etc.) will definitely stay valid until the C function call has returned. Without that special tagging, not even that was guaranteed! The compiler may otherwise release the NSData objects directly after retrieving the byte pointers but prior to making the function call, as it had no knowledge that releasing the data object makes the pointers dangling.

查看更多
霸刀☆藐视天下
4楼-- · 2020-03-30 07:35

The most likely answer is that while string literals are convertible to UnsafePointer<CChar>, and nil is convertible to UnsafePointer<CChar>, and String also is, String? might not be in Swift 2.

查看更多
登录 后发表回答