Swift 5.2, pointers and CoreText

So, I have the following code (simplified for demonstration purposes) to create a CTParagraphStyle:

var minimumLineHeight: CGFloat = 14
var maximumLineHeight: CGFloat = 14
var lineSpacingAdjustment: CGFloat = 10
var alignment: CTTextAlignment = .left

let paragraphStyle = CTParagraphStyleCreate([
    CTParagraphStyleSetting(spec: .minimumLineHeight, valueSize: MemoryLayout<CGFloat>.size, value: &minimumLineHeight),
    CTParagraphStyleSetting(spec: .maximumLineHeight, valueSize: MemoryLayout<CGFloat>.size, value: &maximumLineHeight),
    CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacingAdjustment),
    CTParagraphStyleSetting(spec: .alignment, valueSize: MemoryLayout<CTTextAlignment>.size, value: &alignment)
], 4)

After upgrading to Swift 5.2, I get the warning that Inout expression creates a temporary pointer, but argument 'value' should be a pointer that outlives the call to 'init(spec:valueSize:value:)'.

As far as I can understand I need to replace my code with the following monstrosity:

let minimumLineHeight: CGFloat = 14
let maximumLineHeight: CGFloat = 14
let lineSpacingAdjustment: CGFloat = 10
let alignment: CTTextAlignment = .left

let paragraphStyle: CTParagraphStyle = withUnsafeBytes(of: minimumLineHeight) { minimumLineHeightBytes in
    withUnsafeBytes(of: maximumLineHeight) { maximumLineHeightBytes in
        withUnsafeBytes(of: lineSpacingAdjustment) { lineSpacingAdjustmentBytes in
            withUnsafeBytes(of: alignment) { alignmentBytes in
                CTParagraphStyleCreate([
                    CTParagraphStyleSetting(spec: .minimumLineHeight, valueSize: MemoryLayout<CGFloat>.size, value: minimumLineHeightBytes.baseAddress!),
                    CTParagraphStyleSetting(spec: .maximumLineHeight, valueSize: MemoryLayout<CGFloat>.size, value: maximumLineHeightBytes.baseAddress!),
                    CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: lineSpacingAdjustmentBytes.baseAddress!),
                    CTParagraphStyleSetting(spec: .alignment, valueSize: MemoryLayout<CTTextAlignment>.size, value: alignmentBytes.baseAddress!)
                ], 4)
            }
        }
    }
}

First, is that correct? Is there a better way to write it? I’m assuming that the pointers don’t need to outlive the call to CTParagraphStyleCreate but this isn't actually documented anywhere.

Second, was this always undefined behavior? When is it ever safe to use & to create a pointer to something?

I don’t use CoreText so I don’t really know the requirement, but if your assumption is correct:

Then what you wrote would be correct.

The pointer is valid for the function call. It’s noted in the UnsafeMutablePointer document. It is fine for most APIs, the pointers generally don’t escape the call, but always consult documentation before usage.

The pointer created through implicit bridging of an instance or of an array’s elements is only valid during the execution of the called function.

1 Like

Pointers that need to outlive the call can now be annotated @_nonEphemeral. Near as I can tell, the diagnostic you’re encountering is only triggered when a parameter is marked @_nonEphemeral and you are providing an ephemeral argument (that is, one that doesn’t outlive the call).

There may be some complications arising if this is an Obj-C API where the parameter is considered to have the attribute using some heuristic that isn’t 100% correct; but otherwise, if you’re encountering this diagnostic, I think it’s because the parameter has been explicitly marked as needing a pointer that outlives the call.

1 Like
  1. Yes
  2. I don’t think so

CTParagraphStyle almost certainly makes a copy, but you’re right that it isn’t clearly spelled-out anywhere.

IIUC, the issue is that you’re using &, which is the inout operator, and relying on inout-to-pointer conversion and the scope of the variables to keep them alive until after the CTParagraphStyleCreate functions ends. There are multiple issues with that:

  1. The inout could be replaced with a temporary, so the CTParagraphStyleSetting objects would hold the addresses of those temporaries, which get immediately deinitialised. Then CTParagraphStyleCreate comes by and copies from that deinitalised memory.

  2. Even if inout did reference the variable itself (and not a temporary), lifetimes in swift are not scope-based. The variables could be deinitialised once they are no longer referenced - i.e. as soon as their CTParagraphStyleSetting objects are created (again, making them hold dead memory locations).

1 Like

Yes, it was always undefined. As @lantua notes, the & operator essentially wraps the specific function invocation in a withUnsafePointer(to:) call. In the case of structure initialisers, this is really nasty. Consider the following Swift code:

struct HasAPointer {
    var pointer: UnsafePointer<Int>
}

var x = 5
var myObject = HasAPointer(pointer: &x)

As noted above, the transformation hidden by the & operator means that the last line is actually:

var myObject = withUnsafePointer(to: &x) {
    HasAPointer(pointer: $0)
}

Written this way, it should be clear that this is unsafe. Pointers to Swift objects are valid only for the duration of the with block, but here we escape the pointer outside of that scope.

The result is that it has never been safe to use & in any situation where that pointer may be stored into something that escapes the block. Swift just happens to have gotten better at spotting it.

& remains safe in cases where the pointer truly does not out-live the function call. For example, snprintf writes into a provided buffer: that's a fine use of &.

1 Like

If you want to neaten up the code it would probably be advisable to create a wrapper function around CTParagraphStyleSetting that hides the nasty bits and returns a style setting. Your code then will look similar to your original, but call into the wrapper instead.

1 Like

Huh. That’s interesting. I think my mental model was that variables lived to the end of the current scope, and so, pointers to those variables created using & also lived to the end of the current scope. That was obviously wrong.

This seems to be a very common misconception. Looking at just this specific API, you’ll find more examples of this being called incorrectly on Github than you will find of it being called correctly. In fact, I can't find a single example of it being called correctly.

1 Like

How would a wrapper function that returns a CTParagraphStyleSetting help? As far as I can tell, the setting doesn’t copy the value, it only holds a pointer to it, and that pointer needs to outlive the call to CTParagraphStyleCreate.

Yes, I agree with you. It's no help as far as I can see. I should have looked more closely at that API before chiming in.

1 Like

Thanks for calling this out explicitly because more users could do with understanding it when writing low-level Swift code: that's explicitly not how Swift's variable lifetime works. Swift never guarantees that a variable lives past its last use. As an example, any time after its last use in a function a class-type object may have its deinit called. This is what the function withExtendedLifetime is for: it explicitly guarantees the object will live until at least the end of that block.

Some colleagues of mine are of the opinion that the & operator should never have allowed the creation of a pointer. When you put up GitHub searches like this, you make me think they're probably right.

2 Likes

We should remember this for the next major version of the language. It almost certainly passes the "actively harmful" criteria required for a source-breaking change. Since it now produces a warning, it is already de-facto deprecated (is it officially deprecated?).

3 Likes