Как изменить значение дочернего элемента из зеркального самоанализа

Я делаю кучу BLE в iOS, что означает множество плотно упакованных C-структур, которые кодируются / декодируются как байтовые пакеты. Следующие фрагменты игровой площадки иллюстрируют то, что я пытаюсь сделать в общем.

import Foundation

// THE PROBLEM

struct Thing {
    var a:UInt8 = 0
    var b:UInt32 = 0
    var c:UInt8 = 0
}

sizeof(Thing) // -->   9   :(
var thing = Thing(a: 0x42, b: 0xDEADBEAF, c: 0x13)
var data = NSData(bytes: &thing, length: sizeof(Thing)) // -->   <42000000 afbeadde 13>    :(

Таким образом, учитывая ряд полей различного размера, мы не получаем "самую узкую" упаковку байтов. Довольно хорошо известен и принят. Учитывая мои простые структуры, я хотел бы иметь возможность произвольно кодировать поля вплотную без заполнения или выравнивания. Относительно легко на самом деле:

// ARBITRARY PACKING

var mirror = Mirror(reflecting: thing)
var output:[UInt8] = []
mirror.children.forEach { (label, child) in
    switch child {
    case let value as UInt32:
        (0...3).forEach { output.append(UInt8((value >> ($0 * 8)) & 0xFF)) }
    case let value as UInt8:
        output.append(value)
    default:
        print("Don't know how to serialize \(child.dynamicType) (field \(label))")
    }
}

output.count // -->   6   :)
data = NSData(bytes: &output, length: output.count) // -->   <42afbead de13>   :)

Ура! Работает как положено. Возможно, можно добавить класс вокруг него или расширение протокола и иметь полезную утилиту. Проблема, с которой я столкнулся, заключается в обратном процессе:

// ARBITRARY DEPACKING
var input = output.generate()
var thing2 = Thing()
"\(thing2.a), \(thing2.b), \(thing2.c)" // -->   "0, 0, 0"
mirror = Mirror(reflecting:thing2)

mirror.children.forEach { (label, child) in
    switch child {
    case let oldValue as UInt8:
        let newValue = input.next()!
        print("new value for \(label!) would be \(newValue)")
        // *(&child) = newValue // HOW TO DO THIS IN SWIFT??
    case let oldValue as UInt32: // do little endian
        var newValue:UInt32 = 0
        (0...3).forEach {
            newValue |= UInt32(input.next()!) << UInt32($0 * 8)
        }
        print("new value for \(label!) would be \(newValue)")
        // *(&child) = newValue // HOW TO DO THIS IN SWIFT??
    default:
        print("skipping field \(label) of type \(child.dynamicType)")
    }
}

Учитывая незаполненное значение структуры, я могу соответствующим образом декодировать поток байтов, выяснить, каким будет новое значение для каждого поля. То, что я не знаю, как сделать, это на самом деле обновить целевую структуру с новым значением. В моем примере выше я показываю, как я могу сделать это с C, получить указатель на исходный дочерний элемент, а затем обновить его значение новым значением. Я мог бы сделать это легко в Python/Smalltalk/Ruby. Но я не знаю, как это можно сделать в Swift.

ОБНОВИТЬ

Как предлагается в комментариях, я мог бы сделать что-то вроде следующего:

// SPECIFIC DEPACKING

extension GeneratorType where Element == UInt8 {
    mutating func _UInt8() -> UInt8 {
        return self.next()!
    }

    mutating func _UInt32() -> UInt32 {
        var result:UInt32 = 0
        (0...3).forEach {
            result |= UInt32(self.next()!) << UInt32($0 * 8)
        }
        return result
    }
}

extension Thing {
    init(inout input:IndexingGenerator<[UInt8]>) {
        self.init(a: input._UInt8(), b: input._UInt32(), c: input._UInt8())
    }
}

input = output.generate()
let thing3 = Thing(input: &input)
"\(thing3.a), \(thing3.b), \(thing3.c)" // -->   "66, 3735928495, 19"

По сути, я перемещаю различные методы декодирования потока в байтовый поток (т. Е. GeneratorType, где Element == UInt8), а затем мне просто нужно написать инициализатор, который выводит их в том же порядке, и тип, который определен как структура. Я предполагаю, что эта часть, которая по сути "копирует" само определение структуры (и, следовательно, подвержена ошибкам), - это то, что я надеялся использовать для своего рода интроспекции. Зеркала - единственная настоящая интроспекция Swift, о которой я знаю, и она кажется довольно ограниченной.

2 ответа

Решение

Как обсуждалось в комментариях, я подозреваю, что это слишком умно. Swift включает в себя множество типов, не подходящих для такого подхода. Вместо этого я бы сосредоточился на том, как сделать шаблон как можно проще, не беспокоясь о его устранении. Например, это очень небрежно, но в том направлении, в котором я бы, вероятно, пошел:

Начните с некоторых вспомогательных функций упаковщика / распаковщика:

func pack(values: Any...) -> [UInt8]{
    var output:[UInt8] = []
    for value in values {
        switch value {
        case let i as UInt32:
            (0...3).forEach { output.append(UInt8((i >> ($0 * 8)) & 0xFF)) }
        case let i as UInt8:
            output.append(i)
        default:
            assertionFailure("Don't know how to serialize \(value.dynamicType)")
        }
    }
    return output
}

func unpack<T>(bytes: AnyGenerator<UInt8>, inout target: T) throws {
    switch target {
    case is UInt32:
        var newValue: UInt32 = 0
        (0...3).forEach {
            newValue |= UInt32(bytes.next()!) << UInt32($0 * 8)
        }
        target = newValue as! T
    case is UInt8:
        target = bytes.next()! as! T
    default:
        // Should throw an error here probably
        assertionFailure("Don't know how to deserialize \(target.dynamicType)")
    }
}

Тогда просто позвоните им:

struct Thing {
    var a:UInt8 = 0
    var b:UInt32 = 0
    var c:UInt8 = 0
    func encode() -> [UInt8] {
        return pack(a, b, c)
    }
    static func decode(bytes: [UInt8]) throws -> Thing {
        var thing = Thing()
        let g = anyGenerator(bytes.generate())
        try unpack(g, target: &thing.a)
        try unpack(g, target: &thing.b)
        try unpack(g, target: &thing.c)
        return thing
    }
}

Еще немного мысли могли бы сделать decode метод немного менее повторяющийся, но я все равно, вероятно, продолжу в том же духе, явно перечисляя поля, которые вы хотите кодировать, вместо того, чтобы пытаться проанализировать их. Как вы заметили, интроспекция Swift очень ограничена, и это может продолжаться долго. Он в основном используется для отладки и регистрации, а не логики.

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

Сначала я немного уточнил свою "проблему", включив в нее вложенную структуру:

struct Inner {
    var ai:UInt16 = 0
    var bi:UInt8 = 0
}

struct Thing {
    var a:UInt8 = 0
    var b:UInt32 = 0
    var inner = Inner()
    var c:UInt8 = 0
}

sizeof(Thing) // -->   12   :(
var thing = Thing(a: 0x42, b: 0xDEADBEAF, inner: Inner(ai: 0x1122, bi: 0xDD), c: 0x13)
var data = NSData(bytes: &thing, length: sizeof(Thing)) // -->   <42000000 afbeadde 2211dd13>    :(

Для произвольной упаковки я придерживался того же общего подхода:

protocol Packable {
    func packed() -> [UInt8]
}

extension UInt8:Packable {
    func packed() -> [UInt8] {
        return [self]
    }
}

extension UInt16:Packable {
    func packed() -> [UInt8] {
        return [(UInt8((self >> 0) & 0xFF)), (UInt8((self >> 8) & 0xFF))]
    }
}

extension UInt32:Packable {
    func packed() -> [UInt8] {
        return [(UInt8((self >> 0) & 0xFF)), (UInt8((self >> 8) & 0xFF)), (UInt8((self >> 16) & 0xFF)), (UInt8((self >> 24) & 0xFF))]
    }
}

extension Packable {
    func packed() -> [UInt8] {
        let mirror = Mirror(reflecting:self)
        var bytes:[UInt8] = []
        mirror.children.forEach { (label, child) in
            switch child {
            case let value as Packable:
                bytes += value.packed()
            default:
                print("Don't know how to serialize \(child.dynamicType) (field \(label))")
            }
        }
        return bytes
    }
}

Возможность "упаковать" вещи так же просто, как добавить их к Packable протокол и говорю им pack самих себя. Для моих случаев выше мне нужно только 3 различных типа целых чисел со знаком, но можно добавить гораздо больше. Например, в моем собственном коде у меня есть некоторые Enums получен из UInt8 который я добавил packed метод к.

extension Thing:Packable { }
extension Inner:Packable { }

var output = thing.packed()
output.count // -->   9   :)
data = NSData(bytes: &output, length: output.count) // -->   <42afbead de2211dd 13>   :)

Чтобы иметь возможность распаковывать вещи, я предложил немного поддержки:

protocol UnpackablePrimitive {
    static func unpack(inout input:IndexingGenerator<[UInt8]>) -> Self
}

extension UInt8:UnpackablePrimitive {
    static func unpack(inout input:IndexingGenerator<[UInt8]>) -> UInt8 {
        return input.next()!
    }
}

extension UInt16:UnpackablePrimitive {
    static func unpack(inout input:IndexingGenerator<[UInt8]>) -> UInt16 {
        return UInt16(input.next()!) | (UInt16(input.next()!) << 8)
    }
}

extension UInt32:UnpackablePrimitive {
    static func unpack(inout input:IndexingGenerator<[UInt8]>) -> UInt32 {
        return UInt32(input.next()!) | (UInt32(input.next()!) << 8) | (UInt32(input.next()!) << 16) | (UInt32(input.next()!) << 24)
    }
}

Теперь я могу добавить инициализаторы в мои структуры высокого уровня, например

extension Inner:Unpackable {
    init(inout packed bytes:IndexingGenerator<[UInt8]>) {
        self.init(ai: UInt16.unpack(&bytes), bi: UInt8.unpack(&bytes))
    }
}

extension Thing:Unpackable {
    init(inout packed bytes:IndexingGenerator<[UInt8]>) {
        self.init(a: UInt8.unpack(&bytes), b: UInt32.unpack(&bytes), inner: Inner(packed:&bytes), c: UInt8.unpack(&bytes))
    }
}

Что мне понравилось в этом, так это то, что эти инициализаторы вызывают инициализатор по умолчанию в том же порядке и типах, что и определенная структура. Так что, если структура меняется в типе или порядке, я должен вернуться к (packed:) инициализатор. Дети немного длинные, но не слишком.

Что мне не понравилось в этом, пришлось сдать inout везде. Я, честно говоря, не уверен, какое значение имеют генераторы, основанные на стоимости, так как, передавая их вам, вы почти всегда хотите поделиться состоянием. Единственная цель реификации объекта, который фиксирует положение потока данных, - это возможность поделиться им. Мне также не нравится указывать IndexingGenerator напрямую, но я предполагаю, что есть какая-то магия фу, которая сделает это менее конкретным и все еще работает, но я еще не там.

Я играл с чем-то более питоническим, где я возвращал кортеж типа и остаток переданного массива (а не поток / генератор), но это не было так просто использовать на верхнем уровне init уровень.

Я также попытался поместить статические методы в качестве расширений в генераторы на основе байтов, но вы должны использовать функцию (скорее, использовала бы вычисляемую переменную с побочными эффектами) там, имя которой не соответствует типу, так что вы получите что-то вроде

self.init(a: bytes._UInt8(), b: bytes._UInt32(), inner: Inner(packed:&bytes), c: bytes._UInt8())

Это короче, но не ставит функции типа type рядом с именами аргументов. И потребует добавления всех видов специфичных для приложения имен методов, а также одного расширенного набора UnpackablePrimitives.

Другие вопросы по тегам