قالب وردپرس درنا توس
Home / IOS Development / A Binary Coder for Swift

A Binary Coder for Swift



Friday Q & A 2017-07-28: A Binary Coder for Swift

This article is also available in Hungarian (translation by Zsolt Boros).

In my last article, I discussed the basics of Swift's new Codable protocol, briefly discussing how to implement your own encoder and decoder, and promised another article about a custom binary code that I've been working on. Today I will present the binary code.

Source Code
As usual, source code is available at GitHub:

https://github.com/mikeash/BinaryCoder/tree/887cecd70c070d86f338065f59ed027c13952c83 [19659004] Concept and Approach
This encoder serializes fields by writing them out in order like raw replacements, without metadata. For example: S {
was a : Int1

6
var b : Int32

19659012]: Int64
}

The result of encoding an instance of S is fourteen bytes long, with two bytes for a four bytes for ] b and eight bytes for c . The result is almost the same as printing the raw underlying memory of S except that there is no padding, the numbers are switched to be endian agnostics and it is capable to chase intelligently down references and make custom encoding when needed.

This kind of fair binary coding is a little hobby of me, and I have previously experimented with other approaches to that in Swift, none of which was satisfactory. When Swift 4 beta was available with Codable I saw to see if it would work for this and it did!

My use of Codable is somewhat insulting. I want to take advantage of the compiler-generated Encodable and Decodable implementations, but they use key encoding, while the linear non-metadata binary format is largely the polar opposite of the key encoding. The solution is simple: ignore the keys and trust that the encoding and decoding order is consistent. This is naughty and a bad idea in general, but it works, and even got a tweet from a member of the Swift core team which indicates that it may be OK. This approach is obviously not resistant to changes in field layouts or field types, but as long as you realize this and understand it, it is acceptable.

This means that arbitrary implementation of Codable can not depend on working with this encoder. We know that the compiler-generated implementations work with limitations, but there may be implementations in the default library (such as the implementation for Array ) that rely on semantics that this encoder does not support. In order to ensure that the types do not partipulate in binary encoding without any correction, I created my own binary encoding protocols:

      public    protocol    BinaryEncodable :    Encodable    {
          FUNC    binaryEncode    to    gives :    BinaryEncoder )    casts 
    } 

      public    protocol [19659013] BinaryDecodable :    decodable    {
          init  (  fromBinary    decoding :    BinaryDecoder )    kaster 
    }    ] BinaryCodeable    BinaryCodable    =    BinaryEncodable    &    BinaryDecodable 

I wrote extensions to simplify the usual case where you only want compiler implementation of Codable :

      public    extension    BinaryEncodable    {
          FUNC    binaryEncode [19659014] (  to    gives :    BinaryEncoder ).    throws  
                     Code  (  to :    gives ) 
        }  } 

      public    extension    BinaryDecodable    {
          public    init [19659014] (  fromBinary    decoder :    BinaryDecoder ).    throws [19659000] {
              sample    by itself    init  (19659087) from :    decoder ) 
        } 
    } 

In this way, your own types can only adhere to BinaryCodable and they will get a standard implementation of all they need as long as they meet the requirements. It is required that all fields must be Codable but we can not claim that all fields are BinaryCodable . This type of check must be done by driving, which is unfortunate but acceptable.

Encoder and decoder implementation is simple: they encode / decode everything in order, ignore the keys. The encoder produces bytes that correspond to the values ​​that are encoded, and the decoder provides values ​​from the bytes it has saved.

BinaryEncoder Basic
Encoder is a public class:

      public [19659012]        [   [   [   19659132] UInt8 ]    =    [] 

These data begin blank and bytes are attached when values ​​are encoded.

A convenience method breaks down the process of creating an encoder instance that encodes an object in it and returns the instance data:

      static    func    code  (  _    ]   :         ]    casts    ->    [  uint8 ]    {
          la    gives    =    BinaryEncoder  () 
          sample    value    binaryEncode  (  to :    gives ). 
          return    gives .   data 
    ] 

The encoding process can cast runtime error so the encoder needs an error type:

      enum    Error :    Swift .   error [~] POS = TRUNC    {
          case    typeNotConformingToBinaryEncodable  (  codable .   Type ) 
          case    typeNotConformingToEncodable [19659012] (  All [19659014].   Type ) 
    } 

Let's move on to the low-level codes. We start with a generic method that will code for the raw change of value:

      func    appendBytes   <  T  >  (  of : [19659013] ]  
          
          var    target    =    in 
          withUnsafeBytes  (  to :    &  target )    {
              data    add  (  contentsof .    $   0 ) 
        } 
     ]} 

This will form the basis for other coding methods.

Let's take a quick look at the methods of coding Float and Double next. CoreFoundation has help features that take care of replacements needed for them so that these methods call these features and then call appendBytes with the result:

      func    encode  ( ] _  :     )    {
          appendBytes  (  to :    CFConvertFloatHostToSwapped  (  Value [19659012]] 
    } 

      funk    codes for  {  _    value :    double )    {
          appendBytes [19659014] : :    CFConvertDoubleHostToSwapped  (  value )) 
    } 

While we are in this, here is the method of encoding a Boolean . It refers to Bool to a UInt8 containing a 0 or 1 and codes for:

      func    codes [19659012]       value :    Boolean )    cast    {
          sample    code  (  value   ] 

] ] ] ]

BinaryEncoder has a more

code which deals with the coding of all other codable : ] ] cast {

] This has special cases for different types, so it turns on the parameter:

Int and UInt need special handling because their sizes are not consistent. Depending on the target platform, they may be 32 bits or64 bits. To resolve this, we convert them to Int64 or UInt64 and then encoded the value:

          case    la    v    as    Int [19659013]                      [  Int64  (  v )) 
          case    la    v    as    19659013] : ]          UInt64  (  v ) 

All other integer types are handled with FixedWidthInteger protocol, which shows enough functionality to perform the necessary byte change for encoding values. Because FixedWidthInteger uses Self for some return types, I could not do the job directly here. Instead, I extended FixedWidthInteger with a binaryEncode method that handles the work:

          case    la    v    as    FixedWidthInteger : [19659012] . ]    :         ] 

Float Double ] v ] :
( ] 19659038] v )
case la v as Double : ]
v som Bool :
try ] ]

All that is BinaryEncodable is encoded by calling its binaryEncode method and passing itself ]:

          case    la    binary    as    BinaryEncodable . 
                 binary    binaryEncode  (  to :    even ) 

There is yet another case to handle. Any value that comes so far is not a type as we know how to code native, nor is it BinaryEncodable . In this case, we cast an error to inform the caller that this value does not match the protocol:

          default : 
              discard    Error . ]    encodable ) 
        } 
    } 

Finally, let's look at : FixedWidthInteger extension. All this needs to do is call self.bigEndian to get a portable representation of integer type, then call appendBytes on the encoder to encode this representation:

      private [19659013]] extension    FixedWidthInteger    {
          FUNC    binaryEncode  (  to    gives :    BinaryEncoder )    {   ] encode .   appendBytes  (  by :    by yourself .   bigEndian ) 
        } 
    } 

We now have all important parts of binary coding, but we still do not have an Encoder implementation. To achieve that, we will make implementations of container protocols that call back to BinaryEncoder to perform the work.

BinaryEncoder Encoder Implementation
Let's start by looking at the implementations of the containers. We start with KeyedEncodingContainerProtocol implementation:

      private    struct    KeyedContainer   <  Key :    CodingKey >:   ] KeyedEncodingContainerProtocol    {

The implementation needs a reference to the binary encoder as it works in:

          var    encoder :    BinaryEncoder 

Encoder requires a ] codingPath property that returns a number of CodingKey values ​​indicating current path into the encoder. Since this encoder does not really support keys initially, we always return an empty selection:

          public    var    codingPath :    [  CodingKey ] [19659011] {[19659411] return    []  } 

Code using this class must be implemented in order not to require this value to make sense.

The protocol has so many tones methods of encoding all the different types it supports:

      public    mutating    func    code  (  _    value  ]    Bool ]    Forkey    key .    Even    Key )    casts 
      public    muters    func    code  (  _    value :    Int     Forkey    key .    Self  ]   Key [19659014])    casts 
      public    mutere    fu nc    code  (  _    value :    Int8     Forkey    key .    Even  [19659038] Key )    casts 
      public    muters    FUNC    code  (  _    ver di :    INT16     Forkey    key .    Self    Key )    casts 
      public    muters    func    code [19659014] (  _    value              cast    public    mutere    mutiere                  Key     ]    Code  [ ]    Value :    Int64     Forkey [19659013] Key .    Self     19659013] Key ]    cast 
      public    mutere    func    code  (  _    value :    UINT     Forkey    Key .    Even    Key ) [19659010] Casts 
      Public    Muters    FUNC    Code  Code    ] (  _    value :    uint8     Forkey    key .    Self    Key )    casts 
      public    muters    func    code  (  _ [19659013] value      Key     Cast 
      Public    Mutate    Muting    Muting    Muting        Muting    ] FUNC    Code    _    Value :    uint32     Forkey    Key : [19659010] Self .    Key ]    casts 
      public    mutere    func    code  (  _    value [19659014]:    uint64  ]    Forkey    key .    Self    Key )    casts 
      public    mute    FUNC    code [19659012] (  _    value :    Float  ]    Forkey    key :    Even [19659014]   Key )    casts 
      public    mutere    func    code [19659012]   _    value :    Double     Forkey    key .       Key ]    casts 
      public    muters    func [19659010] code  [ ]    value  :    String     Forkey    Key .    Even    Key )    Casting 
      Public    Muting    Muting    ]          [  T ]  (  [1,965,901 3] value :    T     Forkey    key        Key )    casts    where    T  :    Encodable 

We must complete all these one by one . Let's start with the last one that handles generic Encodable values. It only needs to call
BinaryEncoder
s Code Method:

          Func    Code   <>  19659038] 19659011]                T     Forkey    key :    key  :    Encodable    {
              try    encoder .   code  (  value ) 
        } 

We may use a similar technique to implement the other methods and … what is this?

It appears that this implementation of encode satisfies all of encode methods in protocol because all the other types are Encodable . A suitable generic method will fulfill all corresponding protocol requirements. It's obvious afterwards, but I did not realize it until I finished halfway through this code and saw that errors did not come up when I deleted typeprevious methods.

Now we can see why I implemented the BinaryEncoder s code method with a large switch sentence instead of using separate implementations for all the different supported types . Overloaded methods are solved at compilation time based on static type available on the call page. The above call to encoder.encode (value) will always call func encode (_ encodable: Encodable) although the actual value submitted is a Double ] or a Bool . To allow this simple cover, the implementation in BinaryEncoder must work with a single entry point, which means there must be a large change sentence.

] KeyedEncodingContainerProtocol requires some other methods. There is one for encoding null that we implement to do nothing:

          func    encodeNil  (  for Key    key :    Key ] [19659013] 19659011] {} 

Then there are four methods for returning nested containers or superclass encoders. We do not do anything smart here, so this only delegates back to the encoder:

          func    nestedContainer     NestedKey  [ [  keyedBy    keyType :    NestedKey    Type     Forkey    key  ]    Key )    ->    KeyedEncodingContainer    <  NestedKey >    where    NestedKey  .    CodingKey    {19659013] return    gives    the container  (  keyedBy :    keyType ) 
        } 

          FUNC [19659013] nestedUnkeyedContainer  (  Forkey    key :    key [19659014])    ->    UnkeyedEncodingContainer    {.    return    gives    unkeyedContainer  () 
        } 

          FUNC    superEncoder [19659014] ()    ->    Iver    {19659013]    Return    Provides 
        } 

          FUNC    SuperEncoder  (  Forkey    Key :    Key )    -> [19659000] Encoder    {
              Return    Encoder 
        } 
    } 

We also need implementations of UnkeyedEncodingContainer and SingleValueEncodingContainer . It turns out that these protocols are as large that we can use a single implementation for both. The actual implementation is almost the same as it was for KeyedEncodingContainerProtocol with the addition of a dummy count property:

      private    struct    UnkeyedContanier :    ] UnkeyedEncodingContainer     SingleValueEncodingContainer    {
          var    gives :    BinaryEncoder 

          var    codingPath :    [[19659038] CodingKey    19659011]          return    []  } 

          var    count :    Int    {   return [19659283] 0 [19659075]    KeyedBy    keyType :    NestedKey .                  NestedKey >  Type )    ->    KeyedEncodingContainer   <  NestedKey >    where    NestedKey 
             :    CodingKey    {[19659819] Return    gives    the container  (  keyedBy :    keyType ). 
        ] 
              FUNC    NestedUnkeyedContainer  ()    ->    UnkeyedEncodingContainer    {
              Return    Self 
        } 

          FUNC    SuperEncoder       ->    Encoder [19659011] {
              return    gives 
        } 

          FUNC    encodeNil  ()    casts    {19659013]                  ]  [ ]    value :    T     where    T  :    Encodable    {
            ]  ]    encoder .   encode    value ) 
        } 
    } 

Encoder requires a codingPath property as the containers do:

      ]   ]               :    [     

It also requires a userInfo property. We also do not support it so it returns an empty dictionary:

      public    var    userInfo :    [  CodingUserInfoKey  :    Any ] [Therearethreemethodsthatreturncontainers:

      public    func    19659197] <[19659000]    Key   ]       -> [19659010] KeyedEncodingContainer    []  19659034] Key          where    Key  :    CodingKey    {
          return    KeyedEncodingContainer  (  KeyedContainer   <  Key ]  (  gives :    ) 
    } 

      public    FUNC    unkeyedContainer [19659014] ()    ->    UnkeyedEncodin gContainer    {
         ]   

      

      public    FUNC    singleValueContainer    :    19659012]    -> [19659010]     ] 
    } 

It's the end of BinaryEncoder
UnkeyedContanier [ encoder

    . 
BinaryDecoder Basic
Decoder is a public class too:

      public    class    BinaryDecoder    {

As the encoder it has some data:

      ] fileprivate    la    data :    [  UInt8 ] 

Unlike the encoder, the decoder data is loaded into the object when it is created. The caller provides the data that the decoder will decode:

      public    init  (  data :    [  UInt8 ])    19690103] even  ].   data    =    data 
    } 

The decoder must also keep track of where it is within the data it decodes. It does with a marker property beginning at the beginning of the data:

      fileprivate    var    cursor    =    0 

A convenience method breaks up the process of create a decoder and decode a value:

      static    func    decode   <  T :    BinaryDecodable >  19659038] :    T    Type     data .      ]   ] [19659010] kaster    ->    T    {
          return    sample    BinaryDecoder  (  data :    data ).  decode  (  T .   self ) 
    } 

The decoder has its own mistakes it can throw during the decoding process. Decoding can fail in many more ways than coding, so BinaryDecoder s Error Type has many more things:

      enum    Error :  :    ] .   Error    
          case    prematureEndOfData 
          case    typeNotConformingToBinaryDecodable  (  decodable .   Type ) [19659166] case    case    ]       Type ) 
          case    intOutOfRange  (  Int64 ) 
          case    uintOutOfRange  uint64 ) 
          case    boolOutOfRange  (  uint8 ) 
          case    invalidUTF8  ([[19659132] UInt8  ]]) 
    } 

Now we can move on to the decoding itself. The lowest level method reads a certain number of bytes of data to a pointer, advances the pointer or casts prematureEndOfData if data I do not have enough replace it:

      func   [ ]    byteCount :    Int     in :   ] UnsafeMutableRawPointer ]  ]    throws    {
          if    marker    +    byteCount  >    data .   count    {
              caste    Error .   prematureEndOfData 
        } 

          data .   withUnsafeBytes  ({
             ]    from    = [19659224] $   0.   baseAddress !    +    marker 
              memcpy   ]   to     from  [19659010] byteCount ) 
        }) 

          marker    + =    byteCoun t 
    } [19659021] There is also a small generic package that takes a  inout T  and reads into the value using  MemoryLayout  to determine how many bytes to read.    FUNC    read   <  T   >  (  to :    inout    T )    casts    ] [
         ]        [  MemoryLayout   [  T >    Size ~~ POS = HEADCOMP     to [19659012]. 
          ) 
    } 

As BinaryEncoder BinaryDecoder has methods for decoding floating point types. For disse skapes det en tom CFSwappedFloat verdi, leser inn i den og kaller deretter den riktige CF-funksjonen for å konvertere den til den aktuelle flytpunktstypen:

     func   decode  (.  _   type :   Float   Type )   kaster   ->   Float   {
         Var   byttet   =   CFSwappedFloat32  () 
         prøve   lese  ( til :   &  byttet ) [19659166] returnere   CFConvertFloatSwappedToHost  ( byttet ) 
    } 

     FUNC   DEKODING  ( _   type :   . dobbelt~~POS=TRUNC   Type )   kaster   ->   dobbel   {
         var   byttet   =   CFSwappedFloat64  ( ) 
         p røve   lese  ( inn :   og  SWA ] 
        ] 
         returnerer   CFConvertDoubleSwappedToHost  ( byttet ) 
    } 

Metoden for dekoding Bool dekoder en UInt8 and then returns false if it&#39;s 0true if it&#39;s 1and otherwise throws an error:

    func decode(_[19659013]type: Bool.Type) throws -> Bool {
        switch try[19659010]decode(UInt8.self) {
        case 0: return false
        case  1: return true
        case let x: throw Error.boolOutOfRange[19659014] (x)
        }
    }

The general decode method for Decodable uses a big switch statement to decode various specific types:

    func decode<T: Decodable>(_ type: T.Type) throws -> T {
        switch type {

For Int and UIntit decodes an Int64 or UInt64then converts to an Int or UIntor throws an error:

        case is Int.Type:
            let v = try decode(Int64.self)
            if let v = Int([19660285]exactly: v) {
                return v as![19659010]T
            } else {
                throw Error.intOutOfRange(v)
            }
        case is UInt.Type:
            let v = try decode(UInt64.self)
            if let v = UInt(exactly: v) {
                return v as! T
            } else {
                throw Error.uintOutOfRange(v)
            }

The compiler doesn&#39;t realize that T&#39;s type must match the values being produced, so the as! T convinces it to compile this code.

Other integers are handled through FixedWidthInteger using an extension method:

        case let intT as FixedWidthInteger.Type:
            return try intT.from(binaryDecoder: self) as! T

FloatDoubleand Bool all call their type-specific decoding methods:

        case is Float.Type:
            return try decode(Float.self) as! T
        case is Double.Type:
            return try decode(Double.self) as[19659197]![19659180]T
        case is Bool.Type:
            return try decode(Bool.self) as! T

BinaryDecodable types use the initializer defined in that protocol, passing self:

        case let binaryT as BinaryDecodable.Type:
            return try binaryT.init(fromBinary: self) as! T

If none of the cases are hit, then throw an error:

        default:
            throw Error.typeNotConformingToBinaryDecodable(type)
        }
    }

The FixedWidthInteger method uses Self.init() to make a value, reads bytes i nto it, and then uses the bigEndian: initializer to perform byte swapping:

    private extension FixedWidthInteger {
        static func from(binaryDecoder: BinaryDecoder) throws -> Self {
            var v = Self.init()
            try binaryDecoder.read(into: &v)
            return self.init(bigEndian: v)
        }
    }

That takes care of the foundation. Now to implement Decoder.

BinaryDecoder Decoder Implementation
As before, we implement the three container protocols. We&#39;ll start with the keyed container:

    private struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {

It delegates everything to the decoder, so it needs a reference to that:

        var decoder: BinaryDecoder

The protocol requires codingPath:

        var codingPath: [CodingKey] { return [] }

It also requires allKeyswhich returns all keys that the container knows about. Since we don&#39;t really support keys in the first place, this returns an empty array:

        var allKeys: [Key] { return [] }

There&#39;s also a method to see if the container contains a given key. We&#39;ll just blindly say "yes" to all such questions:

        func contains(_ key: Key) -> Bool {
            return true
        }

As before, KeyedDecodingContainerProtocol has a ton of different decode methods which can all be satisfied with a single generic method for Decodable:

        func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
            return try decoder.decode(T.self)
        }

There&#39;s also a decodeNilwhich we&#39;ll ha ve do noth ing and always succeed:

        func decodeNil(forKey key: Key) throws -> Bool {
            return true
        }

Nested containers and superclass decodes delegate back to the decoder:

        func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
            return try decoder.container(keyedBy: type)
        }

        func nestedUnkeyedContainer(forKey key:[19659180]Key[19659014]) throws -> UnkeyedDecodingContainer {
            return try decoder.unkeyedContainer()
        }

        func superDecoder() throws -> Decoder {
            return decoder
        }

        func superDecoder(forKey key: Key) throws -> Decoder {
            return decoder
        }
    }

Like before, one type can implement both of the other container protocols:

    private struct UnkeyedContainer: UnkeyedDecodingContainer, SingleValueDecodingContainer {
        var decoder: BinaryDecoder

        var codingPath: [CodingKey] { return [][1 9659011]}

        var count: Int? { return nil }

        var currentIndex: Int { return 0 }

        var isAtEnd: Bool { return false }

        func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
            return try decoder.decode(type)
        }

        func decodeNil() -> Bool {
            return true
        }

        func nestedContainer<NestedKey>(keyedBy type: N estedKey[19659014].Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
            return try decoder.container(keyedBy: type)
        }

        func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
            return self
        }

        func superDecoder() throws -> Decoder {
            return decoder
        }
    }

Now BinaryDecoder itself can provide dummy implementations of the properties required by Decoder and implement methods to return instances of the containers:

    public var codingPath: [CodingKey[19659012]][19659011]{ return [] }

    public var userInfo: [CodingUserInfoKey : Any] { return [:] }

    public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
        return KeyedDecodingContainer(KeyedContainer<Key>(decoder: self))
    }

    public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        return UnkeyedContainer[19659012]([19659087]decoder: self)
    }

    public func singleValueContainer() throws -> SingleValueDecodingContainer {
        return UnkeyedContainer(decoder: self)
    }

That is the end of BinaryDecoder.

Array and String Extensions
In order to make the coders more useful, I implemented BinaryCodable for Array and String. In theory I could call through to their Codable implementation, but I can&#39;t count on that implementation to work with the limitations of the binary coders, and I wouldn&#39;t have control over the serialized representation. Instead, I manually implemented it.

The plan is to have Array encode its count, and then encode its elements. To decode, it can decode the count, then decode that many elements. String will convert itself to UTF-8 in the form of Array and then use Array&#39;s implementation to do the real work.

Someday, when Swift gets conditional conformances, we&#39;ll be able to write extension Array: BinaryCodable where Element: BinaryCodable to indicate that Array is is only codable when its contents are. For now, Swift can&#39;t express that notion. Instead, we have to say that Array is always BinaryCodableand then do runtime type checks to ensure the content is suitable.

Encoding is a matter of checking the type of Elementencoding self.countthen encoding all of the elements:

    extension Array: BinaryCodable {
        public func binaryEncode(to encoder: BinaryEncoder) throws {
            guard Element.self is Encodable.Type else {
                throw BinaryEncoder.Error.typeNotConformingToEncodable(Element.self)
            }

            try encoder.encode(self.cou nt[19659012])
            for element in self {
                try (element as! Encodable).encode(to: encoder)
            }
        }

Decoding is the opposite. Check the type, decode the count, then decode that many elements:

        public init(fromBinary decoder: BinaryDecoder) throws {
            guard let binaryElement = Element.self as? Decodable.Type else {
                throw BinaryDecoder.Error.typeNotConformingToDecodable(Element.self)
            }

            let count = try decoder.decode(Int.self)
            self.init()
            self.reserveCapacity(count)
            for _ in 0 ..< count {
                l et decoded = try binaryElement.init(from: decoder)
                self.append(decoded as! Element)
            }
        }
    }

String can then encode itself by creating an Array from its utf8 property and encoding that:

    extension String: BinaryCodable {
        public func binaryEncode(to encoder: BinaryEncoder) throws {
            try Array(self.utf8).binaryEncode(to: encoder)
        }

Decoding decodes the UTF-8 Array and then creates a String[1 9459008] from it. This will fail if the decoded Array isn&#39;t valid UTF-8, so there&#39;s a little extra code here to check for that and throw an error:

        public init(fromBinary decoder: BinaryDecoder) throws {
            let utf8: [UInt8] = try Array(fromBinary: decoder)
            if let str = String(bytes: utf8, encoding: .utf8) {
                self = str
            } else {
                throw BinaryDecoder.Error.invalidUTF8(utf8)
            }
        }
    }

Example Use
That takes care of binary e ncoding and decoding. Use is simple. Declare conformance to BinaryCodablethen use BinaryEncoder and BinaryDecoder on your types:

    struct Company: BinaryCodable {
        var name: String
        var employees: [Employee]
    }

    struct Employee: BinaryCodable {
        var name: String
        var jobTitle: String
        var age: Int
    }

    let company = Company(name: "Joe&#39;s Discount Airbags", employees: [
        Employee(name: "Joe Johnson", jobTitle: "CEO", age: 27[19659012] ),
        Employee(name: "Stan Lee", jobTitle: "Janitor", age: 87),
        Employee(name: "Dracula", jobTitle: "Dracula", age: 41),
        Employee(name: "Steve Jobs", jobTitle: "Visionary", age: 56),
    ])
    let data = try BinaryEncoder.encode(company)
    let roundtrippedCompany = try BinaryDecoder.decode(Company.self, data: data)
    // roundtrippedCompany c ont ains the same data as company

Conclusion
Swift&#39;s new Codable protocols are a welcome addition to the language to eliminate a lot of boilerplate code. It&#39;s flexible enough to make it straightforward to use/abuse it for things well beyond JSON and property list parsing. Unsophisticated binary formats such as this are not often called for, but they have their uses, and it&#39;s interesting to see how Codable can be used for something so different from the built-in facilities. The Encoder and Decoder protocols are large, but judicious use of generics can cut down a lot of the repetitive code, and implementation is relatively simple in the end.

BinaryCoder was written for exploratory and educational purposes, and it&#39;s probably not what you want to use in your own programs. However, there are cases where it could be suitable, as long as you understand the tradeoffs involved.

That&#39;s it for today! Come back again for more exciting byte-related adventures. As always, Friday Q&A is driven by reader ideas, so if you have a topic you&#39;d like to see covered, please send it in!

Did you enjoy this article? I sell all the books full of them! Volumes II and III are now out! They are available as ePub, PDF, Print, and on iBooks and Kindle. Click here for more information.


Comments:

    


Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Code syntax highlighting thanks to Pygments.




Source link