BinaryParseKit: Declarative Binary Parsing with Swift Macros

Hey folks,

I'd like to share BinaryParseKit, a Swift package that uses macros to automatically generate parsing logic for structs and enums from byte arrays.

In my daily work, I often need to parse byte arrays into data structures β€” dozens of them β€” and writing each parser by hand quickly became tedious and messy. To simplify this, I created a macro-based solution that makes parsing more declarative and readable.

When Apple announced the swift-binary-parsing package, I decided to make BinaryParseKit to integrate with it out of the box β€” taking advantage of its memory-safety guarantees and convenience. It's currently supporting iOS 26 and Swift 6.2 and above.

Currently, the macro supports code generation for struct and enum:

@ParseStruct
struct BluetoothPacket {
    @parse
    let packetIndex: UInt8
    @parse
    let packetCount: UInt8
    @parse
    let payload: SignalPacket
}

@ParseStruct
struct SignalPacket {
    @parse(byteCount: 2, endianness: .big)
    let level: UInt32
    @parse(byteCount: 6, endianness: .little)
    let id: UInt64
    @skip(byteCount: 1, because: "padding byte")
    @parse(endianness: .big)
    let messageSize: UInt8
    @parse(byteCountOf: \Self.messageSize)
    let message: String
}

extension String: SizedParsable {
    public init(parsing input: inout BinaryParsing.ParserSpan, byteCount: Int) throws {
        try self.init(parsingUTF8: &input, count: byteCount)
    }
}

Then it can parse a byte array ([UInt8] or Data) by calling BluetoothPacket(parsing: data):

let packet = try BluetoothPacket(parsing: data)

Example:

let data: [UInt8] = [
    // BluetoothPacket header
    0x01,                                          // packetIndex = 1
    0x02,                                          // packetCount = 2
    // SignalPacket (payload)
    0xAA, 0xBB,                                    // level = 43707 (2 bytes of UInt32, big-endian)
    0x56, 0x34, 0x12, 0xEF, 0xCD, 0xAB,            // id = 0xABCDEF123456 (6 bytes, little-endian)
    0x00,                                          // padding byte (skipped)
    0x0D,                                          // messageSize = 13
    0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20,      // "Hello, "
    0x53, 0x77, 0x69, 0x66, 0x74, 0x21             // "Swift!"
]

// Parse it
let packet = try BluetoothPacket(parsing: data)

// Access the fields
print("Packet Index: \(packet.packetIndex)")        // 1
print("Packet Count: \(packet.packetCount)")        // 2
print("Signal Level: \(packet.payload.level)")      // 43707
print("Signal ID: 0x\(String(packet.payload.id, radix: 16))") // 0xabcdef123456
print("Message Size: \(packet.payload.messageSize)") // 13
print("Message: \(packet.payload.message)")          // "Hello, Swift!"

To parse an enum, the macros @ParseEnum and various match macros can be used.

@ParseEnum
enum CommandResult: Equatable {
    @matchAndTake(byte: 0xAA)
    case succeed
    
    @matchAndTake(byte: 0xBB)
    @parse(endianness: .big)
    @parse(endianness: .big)
    case statusUpdate(lat: Float32, long: Float32)
    
    @matchAndTake(byte: 0xCC)
    @parse
    case failure(reason: UInt8)
    
    @matchDefault
    case unknown
}

Parsing works the same way:

let statusUpdateData: [UInt8] = [
    0xBB,                      // statusUpdate command
    0x42, 0x22, 0xD6, 0xE5,    // lat = 40.709858 (Float32, big-endian)
    0xC2, 0x94, 0x03, 0xD7     // long = -74.0075 (Float32, big-endian)
]
let result = try CommandResult(parsing: statusUpdateData)
print("Command Result: \(result)")  // statusUpdate(lat: 40.709858, long: -74.0075)

This project is still in its early stages. I’d really appreciate any feedback or suggestions β€” I’m still exploring how useful this approach might be out side of my own use case. I plan to add support for versions prior to iOS 26 by using withUnsafeBytes along with explicit bounds checking in the near future. There’s also room for performance improvements, such as optimizing bounds checks and making enum matching more efficient through jump tables.

You can find the repository here, and more examples and documentation here.

Thanks for reading β€” I’d love to hear your thoughts, use cases, or ideas for improvement!

9 Likes

How would you add padding bytes at the end of a struct?

It's a dummy example just to demonstrate you can skip some bytes in the parsing process :))

This is excellent, my first idea when I saw the announcement of swift binary parsing was to write this exact kind of package!

BML in javascript by Sergii Kostyrko is an amazing Binary Parser which I lacked when I wrote binary parsers of HoMM3 game files like Sergii did using BML but in Swift (repo Makt).

Do you have support for Bitmasks? Pointfrees parsers are also printers, do you have support for that?

Maybe I can pick up my work on Makt and
rewrite the parser using your package :)

Unfortunately it doesn't support bit masks yet but it's on my future work list. I'm still thinking about what the syntax would look like. Do you have suggestion/opinion on that? :thinking:

The "parsers are printers" is cool! I'll add that to my list! This one can be quickly implemented.

Thanks for the feedback! I really appreciate it :))