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
}
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
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.
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.
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