Hi
So I have a C header I'm using, it has a struct for initialization config, Which takes some integers and a const char *, And after defining the config struct with the values I pass it to the initialization function, By doing something like this
public func initWithConfig(var1: UInt32, var2: UInt32, path: String? = nil) {
var config = syz_LibraryConfig(var1: var1, var2: var2, path: path)
syz_initializeWithConfig(&config)
}
However as you probably guessed, This yields the warning, And it doesn't work properly
C:\Users\mohamed\projects\test\swift_lib\Sources\lib\lib.swift:8:111: warning: passing 'String?' to parameter, but argument 'path' should be a pointer that outlives the call to 'init(var1:var2:path:)'
var config = syz_LibraryConfig(var1: var1, var2: var2, path: path)
^~~~~~~~~~~~~~~
C:\Users\mohamed\projects\test\swift_lib\Sources\lib\lib.swift:8:111: note: implicit argument conversion from 'String?' to 'UnsafePointer?' (aka 'Optional<UnsafePointer>') produces a pointer valid only for the duration of the call to 'init(var1:var2:path:)'
var config = syz_LibraryConfig(var1: var1, var2: var2, path: path)
So, What's the best way to do this? Note I need to make it so if the user didn't provide it with a string value, It becomes nil because the C library internally checks for NULL.
Ok, I fixed it by doing
if let ph = path {
config.path = (ph as NSString).utf8String
}
eskimo
(Quinn “The Eskimo!”)
3
Ok, I fixed it by doing
if let ph = path {
config.path = (ph as NSString).utf8String
}
Oh that’s skating on very thin ice. This works (an Apple platforms at least) because .utf8String returns a buffer that’s placed in the autorelease pool, so the C string point persists until the next cycle of the run loop. If you’re working on an Apple platform on a thread where the run loop is turning and can guarantee that the library taking this configuration structure doesn’t maintain a pointer to the string, you should be fine (-:
Honestly though, if I were in your shoes I’d come up with a better approach. Given that you’re already wrapping this in your initWithConfig(…) function, you can deploy withCString(_:). So something like this:
public func initWithConfig(var1: UInt32, var2: UInt32, path: String) {
path.withCString { pathPtr in
var config = syz_LibraryConfig(var1: var1, var2: var2, path: pathPtr)
syz_initializeWithConfig(&config)
}
}
IMPORTANT This still assumes that syz_initializeWithConfig doesn’t hold on to a pointer to config.path.
FYI, for more background on the challenges here, see my The Peril of the Ampersand DevForums post.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
3 Likes
That's a good idea, However this wouldn't work because path must be unwrapped to String since it's an optional, And I can't do if let or others because the string is being used in syz_initializeWithConfig, So I should duplicate the code or something.
tera
5
Yes, smth like this:
public func initWithConfig(var1: UInt32, var2: UInt32, path: String? = nil) {
if let path = path {
path.withCString { pathPtr in
var config = syz_LibraryConfig(var1: var1, var2: var2, path: pathPtr)
syz_initializeWithConfig(&config)
}
} else {
var config = syz_LibraryConfig(var1: var1, var2: var2, path: nil)
syz_initializeWithConfig(&config)
}
}
It would be easier if you had another syz_initialize, that accepted all parameters individually, then you'd not need withCString.
As Quinn has pointed out: it is very important to know if syz_initializeWithConfig holds a pointer to config.path and uses it at some later point: if this is the case the code above is not good for you.
eskimo
(Quinn “The Eskimo!”)
6
So I should duplicate the code or something.
For a one-off like this that might be the right choice. But if you find yourself doing this a lot, write a helper function:
func withOptionalCString<Result>(_ string: String?, _ body: (UnsafePointer<CChar>?) throws -> Result) rethrows -> Result {
if let string = string {
return try string.withCString(body)
} else {
return try body(nil)
}
}
Or an extension on optional strings:
extension Optional where Wrapped: StringProtocol {
func withOptionalCString<Result>(_ body: (UnsafePointer<CChar>?) throws -> Result) rethrows -> Result {
if let string = self {
return try string.withCString(body)
} else {
return try body(nil)
}
}
}
Another alternative approach is to call strdup:
func myInit(path: String?) {
let pathPtr = path.flatMap { strdup($0) }
defer {
free(pathPtr)
}
var config = MyConfig(path: pathPtr)
myInitC(config: &config)
}
Note that calling free with nil is a guaranteed no-op.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
2 Likes