Historically, there was a solid bridge between Data
and NSData
. Moreover, NSData
and dispatch_data_t
were also bridged [1]. However, the Data
to NSData
bridge is much creakier these days. There are many cases where seemingly benign operations will cause a copy to be made. For this reason, if performance is important and you have the option of using Dispatch data, you should do so.
Consider this program:
import Foundation
func main() {
let p = calloc(3000, 1)!
print("p: \(p)")
let b = UnsafeRawBufferPointer(start: p, count: 3000)
let dd = DispatchData(bytesNoCopy: b, deallocator: .free)
let d = Data(dd)
d.withUnsafeBytes { buf in
print("d: \(buf.baseAddress!)")
}
let n = d as NSData
print("n: \(n.bytes)")
}
main()
If I run in on my machine (macOS 15.2), I see this:
p: 0x000000013000a200
d: 0x000000013000e600
n: 0x000000013000e600
As you can see, the buffer got copied when the program constructs a Data
from the DispatchData
.
This is just one example of this phenomenon. You’ll find these eager copies crop up in all sorts of odd places. You’ll also find that they get elided in various places too. It’s hard to predict exactly when you’ll get a copy and when you won’t, which is why my advice is to use Dispatch data if you can.
For Data, I get an UnsafeMutableRawPointer along with the size I passed, which should allow me to reuse the same Data object, pointing to different memory each time, right?
I’m not sure exactly what your getting at here, but Data
is not an object, it’s a struct, so the question doesn’t make any sense.
If the callback is triggered, is it safe to modify or deallocate the data within it?
That depends on what you mean by “safe”.
First up, an absolute rule: If you construct a data value with a no-copy initialiser, it’s never safe to modify the buffer ‘behind the back’ of the value. For example, this is not safe:
let p = calloc(1024, 1)!
let d = Data(bytesNoCopy: p, count: 1024, deallocator: .custom({ p, _ in free(p)}))
// vvv NOT SAFE vvv
p.storeBytes(of: 1, as: UInt8.self)
// ^^^ NOT SAFE ^^^
print(d)
In the no-copy case, the data value ‘owns’ the buffer until it calls your deallocator.
The above is true for Data
, NSData
, DispatchData
, and dispatch_data_t
. There are no exceptions.
Coming back to the NWConnection.send(…)
case, the behaviour varies by type. Let’s start with dispatch_data_t
. It’s a reference type, but immutable. Being immutable, you can’t modify the data, so that part of the question isn’t valid. That means the only concern is the reference. The reference must be valid when you call send(…)
[2] and must remain valid until send(…)
returns. At that point you can release your reference. If the connection needs to maintain a reference, it will have done that before returning from the send(…)
call.
The situation with DispatchData
is similar. It’s not an object per se, but acts much like one. As with dispatch_data_t
, its contents are immutable. And Swift’s ARC ensures that things are valid for the duration of the send(…)
call.
The Data
type is quite different. It’s mutable, with a copy-on-write (CoW) implementation. Consider this code:
let connection: NWConnection = …
var d = Data("Hello Cruel World!".utf8)
connection.send(content: d, completion: .contentProcessed({ error in
d[1] += UInt8(ascii: "E") // B
}))
d[0] += UInt8(ascii: "h") // A
When the send(…)
returns, the connection has made a copy of the data. However, that’s a CoW copy. If you modify the data at point A, then you could trigger a copy. And that’s true at B as well. Remember that .contentProcessed(…)
means that the data has been enqueued for sending. The connection could still be holding on to its copy in its send buffer.
CoW semantics mean that this isn’t unsafe, but it is a potential performance pitfall.
can I safely deallocate their data since it's already been written to the OS buffer?
If you’re talking about the backing buffer for a no-copy data value then, no, you can’t safely deallocate that. It’s only safe to deallocate it when the system calls the deallocator closure that you supplied when you passed that buffer to the data value’s initialiser. There’s absolutely no relationship between when that deallocator is called and the send(…)
method calls its completion handler.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] In one direction.
[2] Well, nw_connection_send
because we’re talking C at this point.