Reputation: 133189
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?
Upvotes: 2
Views: 1496
Reputation: 133189
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.
Upvotes: 1
Reputation: 540065
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, ifType
isInt8
orUInt8
. 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.
Upvotes: 2
Reputation: 138251
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.
Upvotes: 2