Runtime in Swift: String Lifetimes
September, 2024
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:
- The lifetime of strings passed into C functions (this post)
- Getting my Swift wrapper to be as fast as calling into C directly
- 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; argumentname
must be a pointer that outlives the call toinit(name:value:)
ℹ️ Implicit argument conversion fromString
toUnsafePointer<CChar>
produces a pointer valid only for the duration of the call toinit(name:value:)
ℹ️ Use thewithCString
method onString
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
Any comments or questions about this post? ✉️ nick @ this domain.
— Nick Randall