Why can't I pass an additional Swift String to C function that allows NULL pointers?

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

size_t embeddedSize ( const char *_Nullable string ); 

It makes no sense to use this function 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 to pass an optional string to it! This code will not compile:

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

The compiler throws an error saying that optional does not expand, and Xcode asks me if I can forget to add ! or ? . However, why should I deploy it? NULL or nil are valid values ​​for the function. See above, I just passed nil it and it compiled simply and returned the expected result. In my real code, the string is supplied from the external, and it is optional, I can not get it to expand, which will break if the string was nil . So how can I call this function with an optional string?

+1
source share
3 answers

The most likely answer is that although string literals are converted to UnsafePointer<CChar> , and nil converted to UnsafePointer<CChar> , and String also there, String? may not be in Swift 2.

+2
source

In Swift 2, the C function

 size_t embeddedSize ( const char *_Nullable string ); 

displayed in Swift as

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

and you can pass the (optional) Swift string as an argument, as described in Interacting with the API API in "Using Swift with Cocoa and Objective-C":

Constant pointers

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

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

You can also pass nil because in Swift 2, nil is a valid value for UnsafePointer .

As @zneak noted, "automatic conversion" in UTF-8 does not work for extra lines in Swift 2, so you should (conditionally) expand the line:

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

Using the map Optional method and the nil-coalescing operator ?? , it can be written more compactly, since

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

One common solution was proposed by @zneak .

Here is another possible solution. String has a method

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

which causes a closure by a pointer to a UTF-8 string representation, the lifetime is through f . So for an 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 only the placeholder type to protocols, and not to specific types, we must define the protocol that String matches:

 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 an extra string argument

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

For multiple arguments of a C string, a close 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 is solved in Swift 3. Here the C-function is mapped to

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

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

+1
source

All solutions that are indirect to the call using a faster level work fine if you have only one parameter. But I also have C functions like this ( strX are not 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 indirectness becomes impractical, since I need one indirect relation for each argument, 5 indications of the above function.

Here is the best (ugly) β€œhack” I've come up with so far, which avoids this problem (I'm still glad to see better solutions, maybe someone gets an idea of ​​what the code is):

 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), ) 

If someone is thinking of simply passing the result of data.bytes back to the caller, this is a very bad idea! The pointer returned by data.bytes will be guaranteed to remain valid only as long as data itself is alive, and ARC will kill data as soon as possible. Therefore, the following code is not valid:

 // --- !!! 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 the data remains alive when this method returns; the returned pointer may 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 this is also not guaranteed. The compiler sees that str1 no longer refers when it gets to (*1) , so it may not support it, and when we get to the last line, cstr1 already a dangling pointer.

This is safe, as shown in my first example, since NSData objects ( str1 , etc.) must be supported in the call mode of the calculateTotalLength() function and some methods (for example, bytes of NSData or UTF8String of NSString ) are marked to return an internal pointer, and in this case, the compiler will check that the lifetime of the object extends in the current area while the object or such internal pointer is still referenced. This mechanism ensures that the returned pointers ( str1.bytes , etc.) will definitely remain valid until while calling function C will not come back. It was not even guaranteed without this special marking! The compiler may otherwise free NSData objects immediately after retrieving the byte pointers, but before executing the function call, because it did not know that freeing the data object causes the pointers to hang.

+1
source

Source: https://habr.com/ru/post/1257586/


All Articles