Creating UnsafePointer<CChar> With freeing manually?

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

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.

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

2 Likes