Как работать с буферизованными строками из C в Swift?

Я работаю с саксофонным парсером libxml2 для чтения больших файлов xml. Большинству обработчиков обратного вызова предоставляется указатель char с нулевым завершением. Используя String.fromCString, их можно преобразовать в обычную строку в Swift. Однако sax использует буфер для чтения байтов, поэтому один из обратных вызовов (characters) может вызываться с частью строки, а именно с размером буфера. Эта неполная строка может даже начинаться/заканчиваться на полпути к кодовой точке Unicode. Обратный вызов будет вызываться несколько раз, пока не будет предоставлена ​​полная строка (в кусках).

Я думаю о том, чтобы либо объединить все фрагменты до тех пор, пока не будет собрана полная строка, либо каким-то образом определить границы кодовой точки в частичных строках, обрабатывая только до недопустимой кодовой точки.

Как лучше поступить в таких обстоятельствах? Обработка должна быть максимально быстрой, но при этом корректной. Использование памяти должно быть минимальным, но не за счет производительности.


person Bouke    schedule 19.12.2015    source источник
comment
У меня нет опыта работы с парсером sax libxml2, но здесь сказано, что В вашем обратном вызове вы, вероятно, захотите скопировать символы в какой-то другой буфер.... Например, это можно сделать с помощью NSMutableData.   -  person Martin R    schedule 19.12.2015
comment
Если вы используете String.fromCString, данные кодируются в UTF-8. Вы можете просмотреть последние несколько байтов буфера, чтобы увидеть, где находится граница символа, а затем создать частичную строку, используя NSMutableString(bytes:length:encoding:). Затем сохраните любые дополнительные байты, чтобы добавить их в начало следующего буфера, повторите и добавьте последующие строки в конец оригинала.   -  person Marc Khadpe    schedule 19.12.2015


Ответы (2)


Если скорость обработки является вашей первой целью, я бы просто собирал все символы до тех пор, пока элемент XML не будет полностью обработан и не будет вызван endElement. Это можно сделать с помощью NSMutableData из фреймворка Foundation. Итак, вам нужна недвижимость

var charData : NSMutableData?

который инициализируется в startElement:

charData = NSMutableData()

В обратном вызове characters вы добавляете все данные:

charData!.appendBytes(ch, length: Int(len))

(Здесь допустима принудительная распаковка. charData может быть только nil, если startElement ранее не вызывалась, что означает, что вы допустили ошибку программирования или libxml2 работает некорректно).

Наконец, в endElement создайте строку Swift и выпустите данные:

defer {
    // Release data in any case before function returns
    charData = nil
}
guard let string =  String(data: charData!, encoding: NSUTF8StringEncoding) else {
    // Handle invalid UTF-8 data situation
} 
// string is the Swift string 
person Martin R    schedule 19.12.2015
comment
Спасибо за интересный подход. Есть ли способ сделать это без среды выполнения ObjC? Вместо того, чтобы ждать endElement, можно было бы также проверить последний байт characters на наличие NULL, верно? - person Bouke; 19.12.2015
comment
@bouke: Извините, я не понимаю, что вы имеете в виду. В символах нет NULL. Что вы хотите проверить? (Обратите внимание, что многие функции Swift используют Foundation под капотом, так что эта библиотека все равно используется). – Если вы хотите обнаружить недопустимый ввод до вызова endElement, этот подход не работает, и вам нужно продолжить, например. как в ответе Роба. Это может быть медленнее, потому что данные перемещаются чаще, но вы должны проверить это самостоятельно. - person Martin R; 19.12.2015
comment
Если вас беспокоит копирование производительности, обратите внимание, что обычно проще рассуждать о производительности NSData, чем о производительности [UInt8] (посмотрите в редактировании моей исходной реализации NSData). Массивы — это типы значений с копированием при записи, а также с оптимизацией по одной ссылке, позволяющей избежать копирования. Это затрудняет точное определение того, когда они будут скопированы, если вы манипулируете ими. NSMutableData — это ссылочный тип, поэтому неявные копии никогда не создаются из-за мутации. Я переключился на реализацию [UInt8], потому что код немного проще для чтения, а не потому, что он быстрее. - person Rob Napier; 20.12.2015

Самый длинный допустимый символ UTF-8 составляет 4 байта (RFC 3629, раздел 3). Так что вам не нужен очень большой буфер, чтобы обезопасить себя. Правила того, сколько байтов вам понадобится, тоже довольно просты (просто посмотрите на первый байт). Поэтому я бы просто поддерживал буфер, содержащий от 0 до 3 байтов. Когда у вас есть правильное число, передайте его и попробуйте создать строку. Что-то вроде этого (только слегка протестировано, могут быть угловые случаи, которые все еще не работают):

final class UTF8Parser {
    enum Error: ErrorType {
        case BadEncoding
    }
    var workingBytes: [UInt8] = []

    func updateWithBytes(bytes: [UInt8]) throws -> String {

        workingBytes += bytes

        var string = String()
        var index = 0

        while index < workingBytes.count {
            let firstByte = workingBytes[index]
            var numBytes = 0

                 if firstByte < 0x80 { numBytes = 1 }
            else if firstByte < 0xE0 { numBytes = 2 }
            else if firstByte < 0xF0 { numBytes = 3 }
            else                     { numBytes = 4 }

            if workingBytes.count - index < numBytes {
                break
            }

            let charBytes = workingBytes[index..<index+numBytes]

            guard let newString = String(bytes: charBytes, encoding: NSUTF8StringEncoding) else {
                throw(Error.BadEncoding)
            }
            string += newString
            index += numBytes
        }

        workingBytes.removeFirst(index)
        return string
    }
}

let parser = UTF8Parser()
var string = ""
string += try parser.updateWithBytes([UInt8(65)])

print(string)
let partial = try parser.updateWithBytes([UInt8(0xCC)])
print(partial)

let rest = try parser.updateWithBytes([UInt8(0x81)])
print(rest)

string += rest
print(string)

Это всего лишь один из простых способов. Другим подходом, который, вероятно, будет быстрее, будет обход байтов в обратном направлении в поисках последней точки начала кода (байта, который не начинается с «10»). Затем вы могли бы обработать все до этого момента одним махом и выделить в особом регистре только последние несколько байтов.

person Rob Napier    schedule 19.12.2015
comment
Интересный подход, только перенос оставшихся байтов, вероятно, улучшит производительность, требуя меньшего копирования. - person Bouke; 19.12.2015
comment
Конечно, вы можете создать больше особых случаев, чтобы избежать еще большего копирования. Например, поскольку вы знаете, сколько байтов вы хотите из bytes, вы можете отрезать их и добавить к workingBytes, а затем работать с остальными как срез. По моему опыту, если копирование вызывает серьезную озабоченность, то NSMutableData — лучший инструмент, потому что он имеет гораздо более предсказуемые правила копирования и не имеет оптимизации noCopy для работы с внешними буферами. Вы также можете работать с UnsafeMutableBufferPointers, но их также может быть очень сложно использовать правильно. - person Rob Napier; 20.12.2015
comment
См. robnapier.net/nsdata, где я много разглагольствовал об уходе и возвращении на NSMutableData, почти исключительно из-за проблем с копированием. - person Rob Napier; 20.12.2015