Search code examples
iosswiftsqliteunsafe-pointerspencilkit

Convert Data into UnsafeRawPointer and vice versa in swift


I have been working on an app that uses Pencil Kit and I am trying to store a drawing on the canvas into a sqlite3 database. In order to do so, I had to convert the drawing(type: Data) into an UnsafeRawPointer. However, after the conversion, when I try to access (print) the drawing via the pointer, it returns 0 bytes instead of 42 bytes. I have added some print statements and what they return in the code below, I hope that helps.

 // Function that converts drawing data to UnsafeRawPointer
 func dataToPtr(drawing: Data) -> UnsafeRawPointer {
     
     let nsData = drawing as NSData
     print(nsData) // shows 42 bytes

     let rawPtr = nsData.bytes
     print(rawPtr.load(as: Data.self))// Shows 0 bytes
     return rawPtr
 }

// Drawing before conversion
print(canvas.drawing) // Prints: 42 bytes

let drawingPtr = dataToPtr(drawing: canvas.drawing)

// Drawing when accessing the pointer
print(drawingPtr.load(as: Data.self)) // shows 0 bytes

I am a beginner in making iOS app and have struggle to understand pointers in swift. Thank you in advance.

Edit: The save drawing method:

func save(canvas: Canvas) {
    connect()
    
    // prepare
    var statement: OpaquePointer!
    
    // update the drawing given the row id
    if sqlite3_prepare_v2(database, "UPDATE drawings SET drawing = ? WHERE rowid = ?", -1, &statement, nil) != SQLITE_OK {
        print("Could not create (update) query")
    }
    
    // bind place holders
    print("DRAWING SAVED: \(canvas.drawing)") // shows 42 bytes
    let drawingPtr = dataToPtr(drawing: canvas.drawing)

    sqlite3_bind_blob(statement, 1, drawingPtr, -1, nil)
    sqlite3_bind_int(statement, 2, Int32(canvas.id))
    
    // execute
    if sqlite3_step(statement) != SQLITE_DONE {
        print("Could not execute update statement")
    }
    
    // finalise
    sqlite3_finalize(statement)
}

The method where I wanted to convert the pointer to data using .load():

// Function to check if canvas for a certain date is already in the database, if exists, return canvas
func check(selectedDate: Date) -> [Canvas] {
    connect()
    
    var result: [Canvas] = []

    // prepare
    var statement: OpaquePointer!

    if sqlite3_prepare_v2(database, "SELECT rowid, date, drawing FROM drawings WHERE date = ?", -1, &statement, nil) != SQLITE_OK {
        print("Could not create (select) query")
        return []
    }
    
    // bind
    sqlite3_bind_text(statement, 1, NSString(string:dateToStringFormat(dateDate: selectedDate)).utf8String, -1, nil)
    
    // executes
    while sqlite3_step(statement) == SQLITE_ROW {
        
        // change string date into Date date
        let Date_date = stringToDateFormat(stringDate: String(cString: sqlite3_column_text(statement, 1)))
        // if canvas is not empty
        if sqlite3_column_blob(statement, 2) != nil {
            let drawingPtr = sqlite3_column_blob(statement, 2)
            
            result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawingPtr!.load(as: Data.self)))
            print("DRAWING NOT NIL")
        }
        else {
            let drawing = Data.init()
            result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawing))
            print("DRAWING IS NIL")
        }
    }
    // finalise
    sqlite3_finalize(statement)
    
    return result
}

Solution

  • What you need to do here is wrap your function body in a withUnsafeBytes:

    func save(canvas: Canvas) {
        connect()
    
        let drawingData = canvas.drawing.dataRepresentation()
    
        drawingData.withUnsafeBytes { drawingBuffer in
    
            let drawingPtr = drawingBuffer.baseAddress!
    
            // ... In here you can use drawingPtr, for example:
    
            sqlite3_bind_blob(statement, 1, drawingPtr, Int32(drawingBuffer.count), nil)
    
            // ...
        }
    }
    

    Inside of the withUnsafeBytes block you must not refer to drawingData itself. Outside of the block, you must not refer to drawingPtr.

    The point of withUnsafeBytes is that it ensures there is a contiguous representation of the bytes (making copies if needed), and then provides you a pointer to those bytes that is valid for the duration of the block. This pointer is not valid outside of the block. You must not return it or let it otherwise escape. But within the block, you may use it as a void *. This means you must make sure that sqlite3 does not store drawingPtr past the end of this block, which is why you must put the withUnsafeBytes around the entire prepare/finalize sequence, not just the bind_blob statement.

    As a rule, you cannot pass around UnsafeRawPointers to things that you did not allocate yourself. There is no promise that the thing they point to continues to exist when you think it does. In the case of Data, there isn't even a promise that it represents a single block of memory (Data may be backed from a dispatch_data, for example). The way that you access a Data's bytes is using withUnsafeBytes.

    Your check function has some mistakes, as well. First, your NSString conversions are unnecessary. This line:

    sqlite3_bind_text(statement, 1, NSString(string:dateToStringFormat(dateDate: selectedDate)).utf8String, -1, nil)
    

    Can be written as just:

    sqlite3_bind_text(statement, 1, dateToStringFormat(dateDate: selectedDate), -1, nil)
    

    Swift will automatically convert String to C-string when passed to a C function that takes a char *.

    This code is simply wrong, and may be why you're getting zero bytes:

    let drawingPtr = sqlite3_column_blob(statement, 2)
    result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawingPtr!.load(as: Data.self)))
    

    A pointer to a Blob is not a Data. You can't just load it this way. You need to know how long it is. This is the code you need there:

    // Get the pointer
    let drawingPtr = sqlite3_column_blob(statement, 2)!
    
    // Get the length
    let drawingLength = Int(sqlite3_column_bytes(statement, 2))
    
    // Copy the bytes into a new Data
    let drawing = Data(bytes: drawingPtr, count: drawingLength)
    
    // Now construct your Canvas.
    result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawing))