Search code examples
swiftreflectionswift-structs

How to change the value of a child from a Mirror introspection


I'm doing a bunch of BLE in iOS, which means lots of tight packed C structures being encoded/decoded as byte packets. The following playground snippets illustrate what I'm trying to do generically.

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>    :(

So given a series of fields of varying size, we don't get the "tightest" packing of bytes. Pretty well known and accepted. Given my simple structs, I'd like to be able to arbitrarily encode the fields back to back with no padding or alignment stuff. Relatively easy actually:

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

Huzzah! Works as expected. Could probably add a Class around it, or maybe a Protocol extension and have a nice utility. The problem I'm up against is the reverse process:

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

Given an unpopulated struct value, I can decode the byte stream appropriately, figure out what the new value would be for each field. What I don't know how to do is to actually update the target struct with the new value. In my example above, I show how I might do it with C, get the pointer to the original child, and then update its value with the new value. I could do it easily in Python/Smalltalk/Ruby. But I don't know how one can do that in Swift.

UPDATE

As suggested in comments, I could do something like the following:

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

Basically, I move the various stream decoding methods to byte stream (i.e. GeneratorType where Element == UInt8), and then I just have to write an initializer that strings those off in the same order and type the struct is defined as. I guess that part, which is essentially "copying" the structure definition itself (and therefore error prone), is what I had hoped to use some sort of introspection to handle. Mirrors are the only real Swift introspection I'm aware of, and it seems pretty limited.


Solution

  • As discussed in the comments, I suspect this is over-clever. Swift includes a lot of types not friendly to this approach. I would focus instead on how to make the boilerplate as easy as possible, without worrying about eliminating it. For example, this is very sloppy, but is in the direction I would probably go:

    Start with some helper packer/unpacker functions:

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

    Then just call them:

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

    A little more thought might be able to make the decode method a little less repetitive, but this is still probably the way I would go, explicitly listing the fields you want to encode rather than trying to introspect them. As you note, Swift introspection is very limited, and it may be that way for a long time. It's mostly used for debugging and logging, not logic.