Hello,
I recently wanted to access stable pointers into a C string built from a Swift string, and I wanted to ask if there were a better way to do it.
My question involves parsing through a low-level C api, so it may be of general interest.
The use case is the following: I want to compile multiple SQL statements joined with semicolons with an SQLite API (sqlite3_prepare_v3
) that accepts a const char *zSql
as the beginning of a C string, and a const char **pzTail
as an output pointer to the "unused portion of zSql" (read: everything after the compiled statement):
"INSERT INTO ...; DELETE ...;"
^ zSQL ^ pzTail
In order to compile multiple statements, one uses the pzTail
value as the zSQL
on the next call to sqlite3_prepare_v3
(until the string is exhausted).
It is thus important to have a C string which is stable in memory across the multiple calls to sqlite3_prepare_v3
, so that pointers point to the expected characters.
Swift strings can generate a C string with utf8CString
, which returns a ContiguousArray
.
A contiguous array is "a specialized array that always stores its elements in a contiguous region of memory." This looks like a good match: I can expect the C string to be located at a stable location in memory :-)
But it happens both withUnsafeBufferPointer(_:)
and withUnsafeBytes(_:)
methods of ContiguousArray document that:
The pointer argument is valid only for the duration of the method’s execution.
It also happens that I wanted to design an API where it is the user who decides if the iteration of SQL statement should stop or continue:
// User code
let statements = database.allStatements(sql: "...")
while let statement = try statements.next() {
// use statement, break, etc
}
This means that I can not iterate all statements in a single stroke. I really need a stable C string for an unknown amount of time (until all statements are iterated, or the user decides the iteration should stop).
I ended up performing a copy of the C string:
// User input:
let sql: String
// Initialization of the iteration:
// Build a C string and copy it into a stable buffer
let cString = sql.utf8CString
let buffer: UnsafeBufferPointer<CChar> = cString.withUnsafeBytes { rawBuffer in
let copy = UnsafeMutableRawBufferPointer.allocate(
byteCount: rawBuffer.count,
alignment: MemoryLayout<CChar>.alignment)
copy.copyMemory(from: rawBuffer)
return UnsafeBufferPointer(copy.bindMemory(to: CChar.self))
}
// Later when the iteration has ended:
buffer.deallocate()
This copy makes me sure the C string is at a stable location in memory, and I can invoke sqlite3_prepare_v3
several times with the result of its previous invocations, moving forward a pointer initialized at buffer.baseAddress
.
Does anyone know if it is possible to avoid this copy?
EDIT: I could use offsets instead of stable pointers (by subtracting baseAddress
from pzTail
and using this offset in order to build a new pointer from the contiguous array on the next step of the iteration). This would be a valid solution. But how come C developers don't face the same problems and workarounds ?