Runtime in Swift: String Lifetimes

Wed, 11 Sep 2024 16:43:00 +0200

Some lessons learned while building a Swift API for the ObjC runtime functions, part 1.

This series of posts is about some interesting problems I encountered while building a seemingly simple Swift wrapper around the Objective-C runtime. The three parts are:

1. The lifetime of strings passed into C functions (this post)

2. Getting my Swift wrapper to be as fast as calling into C directly

3. Exposing some runtime-related classes that are marked as unavailable in Swift

Why a Swift API for the Objective-C runtime?

While I still like writing about the Objective-C runtime, I’ve almost exclusively used Swift for the past few years so my skills in actually coding Objective-C have gotten a bit rusty. Building this Swift wrapper is my way of splitting the two so I can play with the runtime without having to use the language. Admittedly it’s of limited practical use but at some point it just became a challenge to finish.

Most of the Swift wrappers are pretty trivial and essentially just rename the C functions and convert the types if necessary. For example:

struct ObjCMethod {
    let method: Method

    var argCount: Int {
        Int(method_getNumberOfArguments(method))
    }
}

I initially wanted to add these methods as an extension on the runtime’s Method type itself instead of wrapping it in a struct. When I then started working on the Ivar extension I would get weird “Invalid redeclaration” warnings which was really confusing. Eventually I realised that a lot of these runtime types (including Method and Ivar) are just aliases for OpaquePointer and not actually distinct types, so no wonder the compiler was getting upset.

The first real challenge I ran into was related to bridging strings between Swift and C.

Lifetime of string parameters

Passing a Swift String into a C function is usually straightforward. Take a runtime C function like objc_lookUpClass which returns a class by name:

Class objc_lookUpClass(const char *name);

This gets bridged to Swift like so:

func objc_lookUpClass(_ name: UnsafePointer<CChar>) -> AnyClass?

While the UnsafePointer might look a bit concerning, Swift lets you call this in a very natural way by just using a String:

let clsName = "NSDictionary"

let cls = objc_lookUpClass(clsName)

Under the hood, Swift is creating a temporary UnsafePointer to the String contents and passing it to the C function for us. This works fine as long as the C function does not try to store the pointer and use it after the function has returned.

It gets a little more complicated when the function returns a new C string. In that case we can’t just convert it to a Swift String, we also have to free the returned pointer. Take this function which gets the encoding of a method’s return type:

/// Returns a string describing a method's return type.
/// You must free the string with free().
func method_copyReturnType(_ m: Method) -> UnsafeMutablePointer<CChar>

Because the function name includes “copy” it means we’re getting a newly-allocated string and are responsible for freeing it:

let ptr = method_copyReturnType(method)
defer { free(ptr) }
let returnType = String(cString: ptr)

So far so good, but where I got stuck was needing to bridge strings contained in other types.

Arrays of structs of strings

This runtime function dynamically adds a new property to some class:

func class_addProperty(
    _ cls: AnyClass?,
    _ name: UnsafePointer<CChar>,
    _ attributes: UnsafePointer<objc_property_attribute_t>?,
    _ attributeCount: UInt32
) -> Bool

That attributes parameter is an array of objc_property_attribute_t structs, each of which contains two pointers to strings:

struct objc_property_attribute_t {
    var name: UnsafePointer<CChar>
    var value: UnsafePointer<CChar>
}

You might think you can just call this function like so:

let attribs = [
    objc_property_attribute_t(name: "T", value: "@"),
    objc_property_attribute_t(name: "R", value: ""),
    objc_property_attribute_t(name: "V", value: "_city"),
]

class_addProperty(cls, "city", attribs, UInt32(attribs.count))

But this is just going to get you a bunch of warnings:

⚠️ Cannot pass String to parameter; argument name must be a pointer that outlives the call to init(name:value:)
ℹ️ Implicit argument conversion from String to UnsafePointer<CChar> produces a pointer valid only for the duration of the call to init(name:value:)
ℹ️ Use the withCString method on String in order to explicitly convert argument to pointer valid for a defined scope

These warnings are legitimate: by the time you’ve created a objc_property_attribute_t the strings that it points to may have been deallocated. The proper way to do this in Swift would be to use the suggested withCString function and only access the string contents in a closure:

let name = "T"
let value = "@"

name.withCString { namePtr in
    value.withCString { valuePtr in
        let attrib = objc_property_attribute_t(name: namePtr, value: valuePtr)
    }
}

Note that we still can’t use attrib outside of these closures because it’ll contain invalid pointers. As the documentation for withCString says:

The pointer passed as an argument to body is valid only during the execution of withCString(_:). Do not store or return the pointer for later use.

This could work if we were only dealing with a single property attribute since we can call class_addProperty inside the closures. But how are we meant to deal with an array of attributes of any length? There’s no way to use an unknown number of nested calls to withCString!

Arrays of string pointers

A useful article on the Apple dev forums by the Eskimo called The Peril of the Ampersand explains how to handle this situation (see the Manual Memory Management section at the bottom). I only came across it while I was writing this post, but at least it confirmed that there was no easy way of doing this using standard Swift. The suggested technique is to use strdup to create a copy of all the strings, add them to an array of pointers and free them all afterwards. The solution I’d already come up with was similar but didn’t rely on any manual memory management.

Given an array of name/value string pairs, I wanted to be able to call it like this:

let attribs = [("T", "@"), ("R", ""), ("V", "_city")]

withAttributes(attribs) { objCAttribs in
    class_addProperty(cls, "city", objCAttribs, UInt32(objCAttribs.count))
}

What I did was concatenate all the null-terminated name and value strings into a single string so that we only need one withUnsafe call. By accessing the pointer to that joined string, I can create an array of objc_property_attribute_t and use them within the closure:

func withAttributes(
    _ attribs: [(String, String)],
    block: ([objc_property_attribute_t]) -> Void
) {
    // Concatenate all the name/value pairs as C strings into a single CChar array
    let cStrings = attribs.flatMap {
        $0.0.utf8CString + $0.1.utf8CString
    }

    // Access the underlying memory of the joined strings
    cStrings.withUnsafeBufferPointer { buffer in
        var strPtr = buffer.baseAddress!

        // Create an array of objc_property_attribute_t
        let objCAttribs = attribs.indices.map { _ in
            objc_property_attribute_t(
                name: strPtr.moveAfterNextNull(),
                value: strPtr.moveAfterNextNull()
            )
        }

        // Call the closure with the array of objc_property_attribute_t
        block(objCAttribs)
    }
}

That moveAfterNextNull function is an extension I added to C strings to advance the pointer to the next string in memory. It searches for the next null byte and then moves one byte past that to the start of the next string. You can also use a C function like strchr to search for the null byte although I don’t think it would make much difference.

extension UnsafePointer<CChar> {
    /// Moves the pointer to the start of the next C string,
    /// but returns the original pointer
    mutating func moveAfterNextNull() -> Self {
        let original = self
        while self.pointee != 0 {
            self += 1
        }
        self += 1   // Move past the null to the next string
        return original
    }
}

This is probably the deepest I’ve needed to dig into unsafe string buffers which says a lot about how decent Swift’s C bridging usually is. It was also a comforting reminder about how strictly Swift controls the lifetime of objects and how difficult the language makes it to shoot yourself in the foot.

Maybe there’s an easier way to do this though? Let me know if you can come up with one.

Useful links

Next post: Getting my Swift wrapper to be as fast as calling into C directly

Final post: Exposing some runtime-related classes that are marked as unavailable in Swift

Dev forums thread on the ampersand operator in Swift and lifetime of references