1

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
}
Clare
  • 71
  • 6
  • Please show your canvas type and drawing declarations. A `canvasView.drawing` property should return a `PKDrawing` not `Data` – Leo Dabus Oct 04 '20 at 18:32
  • If you need to save a `PKDrawing` you need to use its dataRepresentation method. `canvas.drawing.dataRepresentation()` – Leo Dabus Oct 04 '20 at 18:55
  • Hi, sorry I didn't make that clear. I have already performed .dataRepresentation on the drawing so it should have type data. – Clare Oct 04 '20 at 20:23
  • The code you've written here is undefined behavior. You're probably solving the problem incorrectly. I strongly suspect you just need an `&` in your call rather than what you're doing, but it's possible you need a `withUnsafeBytes`. Can you show the sqlite3 call you're trying to make? (Specifically, what the function signature is?) – Rob Napier Oct 05 '20 at 03:20
  • (The key point is that you will not actually convert the Data to an UnsafeRawPointer. You will, instead, have the Data make an UnsafeRawPointer available for passing to a function. I know that sounds like a very small difference, but it is a very important difference in how Swift manages memory. You cannot just hand around UnsafeRawPointers. The thing they point to may disappear at any time unless you use the right tools.) – Rob Napier Oct 05 '20 at 03:25
  • Hi Rob, I have updated the question including the sqlite statement. Thank you. – Clare Oct 05 '20 at 15:25
  • How do you make an unsafeRawPointer available? and will I still have the problem of converting the pointer into data using .load when i try to retrieve the data? – Clare Oct 05 '20 at 16:02
  • @Clare you need to fetch the size of your blob as well `let count = sqlite3_column_bytes(statement, 2)` and use the Data initializer – Leo Dabus Oct 05 '20 at 16:10

1 Answers1

2

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))
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    `drawing` returns a `PKDrawing` not `Data`. You meant `canvas.drawing.dataRepresentation().withUnsafeBytes { drawingPtr in` – Leo Dabus Oct 05 '20 at 16:14
  • The most important part as I mentioned in my answer is to use the `Data` initializer instead of `load(as: Data.self)` and use `sqlite3_column_bytes` to get the bob size as I commented above. – Leo Dabus Oct 05 '20 at 16:32
  • @RobNapier Thank you for your detailed explanation! I have wrapped my save method inside the withUnsafeBytes as you have suggested and came across a problem. An error popped up for the line: "sqlite3_bind_blob(statement, 1, drawingPtr, -1, nil)" . The error is: "Cannot convert value of type 'UnsafeRawBufferPointer' to expected argument type 'UnsafeRawPointer?'" I couldn't find anyway to convert the UnsafeRawBufferPointer into an UnsafeRawPointer. Do you have any suggestions to how to fix this error? Thank you again. – Clare Oct 06 '20 at 17:29
  • I apologize; I thought I checked this code more closely than I must have. See my edit where I unwrap the buffer into a pointer: `let drawingPtr = drawingBuffer.baseAddress!` A "buffer pointer" just combines a pointer and a length into a single collection, which is often nicer to work with. – Rob Napier Oct 06 '20 at 18:10
  • @RobNapier Thank you for your explanation, that fixed the error message. However, this hasn't fixed my problem with saving the drawing. In the check method above, the "sqlite3_column_blob(statement, 2)" at the 2nd if statement always returns nil so the code never actually tries to load the data. Is there a reason why it is behaving this way? – Clare Oct 07 '20 at 18:02
  • I don't know what you mean here by "at the 2nd if statement." If you've changed the code significantly, you'll need to include that in the question, or open a new question if the issue is different. (Think about what future searchers with similar problems will be looking for, and whether they also will find a useful answer here.) – Rob Napier Oct 07 '20 at 18:08
  • @RobNapier Sorry for being unclear, I don't really know what the problem is but I thought the question is still related to this topic as it is still to do with the pointer. – Clare Oct 07 '20 at 18:59
  • In the check method, I used an if statement to check if "sqlite3_column_blob(statement, 2)", the pointer, returns nil (because it causes an error in "let drawingPtr = sqlite3_column_blob(statement, 2)!" if it is nil). It turns out that the "sqlite3_column_blob(statement, 2)", always returns nil even when I thought I have successfully saved the drawing. I might be completely wrong but I thought it might be to do with not accessing the pointer in an withUnsafeBytes block again. – Clare Oct 07 '20 at 19:00
  • Read the SQLite database directly to see what's in it. There are tools like Base, DataGrip, or just `sqlite3` from the commandline that will let you query the database directly. – Rob Napier Oct 07 '20 at 19:28
  • I queried the database and the drawing column is empty. – Clare Oct 07 '20 at 19:40
  • So that tells you the mistake is in `save()`, not `check()`. – Rob Napier Oct 07 '20 at 20:46
  • @RobNapier `sqlite3_bind_blob(statement, 1, drawingPtr, -1, nil)` should be `sqlite3_bind_blob(statement, 1, drawingPtr, Int32(drawingBuffer.count), nil)` https://stackoverflow.com/a/64389704/2303865 – Leo Dabus Oct 16 '20 at 16:10