Converta dados em UnsafeRawPointer e vice-versa rapidamente

Oct 05 2020

Tenho trabalhado em um aplicativo que usa o Pencil Kit e estou tentando armazenar um desenho na tela em um banco de dados sqlite3. Para fazer isso, tive que converter o desenho (tipo: Data) em um UnsafeRawPointer. Porém, após a conversão, quando tento acessar (imprimir) o desenho através do ponteiro, ele retorna 0 bytes em vez de 42 bytes. Eu adicionei algumas instruções de impressão e o que elas retornam no código abaixo, espero que ajude.

 // 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

Eu sou um novato em fazer aplicativos iOS e tenho dificuldade para entender as dicas rapidamente. Agradeço antecipadamente.

Editar: O método para salvar o desenho:

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)
}

O método em que eu queria converter o ponteiro em dados usando .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
}

Respostas

2 RobNapier Oct 05 2020 at 22:59

O que você precisa fazer aqui é envolver o corpo da função em 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)

        // ...
    }
}

Dentro do withUnsafeBytesbloco você não deve se referir a drawingDatasi mesmo. Fora do bloco, você não deve se referir drawingPtr.

O objetivo withUnsafeBytesé garantir que haja uma representação contígua dos bytes (fazendo cópias, se necessário) e, em seguida, fornecer um ponteiro para esses bytes que é válido para a duração do bloco. Este ponteiro não é válido fora do bloco. Você não deve devolvê-lo ou deixá-lo escapar de outra forma. Mas dentro do bloco, você pode usá-lo como um void *. Isso significa que você deve certificar-se de que sqlite3 não armazene drawingPtralém do final deste bloco, e é por isso que você deve colocar o em withUnsafeBytestorno de toda a sequência de preparação / finalização, não apenas a bind_blobinstrução.

Como regra, você não pode passar UnsafeRawPointers para coisas que você não alocou. Não há promessa de que aquilo para o qual eles apontam continuará existindo quando você pensa que existe. No caso de Data, não há nem mesmo a promessa de que ele represente um único bloco de memória (os dados podem ser copiados de um dispatch_data, por exemplo). A forma como você acessa os bytes de um Data está usando withUnsafeBytes.

Sua checkfunção também contém alguns erros. Primeiro, suas conversões NSString são desnecessárias. Está linha:

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

Pode ser escrito apenas como:

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

O Swift converterá automaticamente String em string C quando passado para uma função C que recebe um char *.

Este código está simplesmente errado e pode ser o motivo pelo qual você está obtendo 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)))

Um ponteiro para um Blob não é um Data. Você não pode simplesmente loadassim. Você precisa saber quanto tempo é. Este é o código de que você precisa:

// 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))