Skip to content

Commit

Permalink
Basic docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Joannis committed Apr 10, 2024
1 parent 5ce89ea commit 7871588
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 26 deletions.
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,71 @@
A small library, similar to Codable, that uses macro annotations to generate a binary format. This format can be stored on disk, or communicated over a network.

## Defining Types

```swift
import SwiftBin

@BinaryFormat struct UserSession {
let iosVersion: Int
let username: String
var actions: [UserAction]
}

@OpenBinaryEnum enum UserAction {
// Supports regular enum cases
case appMovedToForeground, appMovedToBackground

// Also supports associated values
case searchInTimeline(String)
case sendChatMessage(String, toUserId: String)

// Adding values at the end of an `OpenBinaryEnum` is non-breaking, adding new cases elsewhere is breaking.
}
```

### Foundation Support

You can use convenience APIs to write the buffer into `Data`. This works well for iOS apps.

```swift
var session = UserSession(..)
let data = try session.writeData()
```

You can use convenience APIs for parsing as well.

```swift
let parsedSession = try UserSession(parseFrom: data)
```

## Bring your own types!

SwiftNIO is used on the Server, and who knows what's next! Types are serialized into a `BinaryWriter`. You can create your own BinaryWriter that sends the data over a socket, into SwiftNIO ByteBuffer or writes it to the FileSystem.

```swift
var writer = BinaryWriter { data in
// Callback is triggered for `data` that needs to be written
// This can throw an error
}

try session.serialize(into: writer)
```

You can flush on-write, buffer it yourself, or flush after use.

### Read from Buffers

Likewise, `BinaryBuffer` can be used to represent data. It's initialized with a pointer and count, allowing it to be used with most data types.

```swift
let userSession = try data.withUnsafeBytes { buffer in
let buffer = buffer.bindMemory(to: UInt8.self)
var binary = BinaryBuffer(
pointer: buffer.baseAddress!,
count: buffer.count,
release: nil
)

return try UserSession(consuming: &binary)
}
```
113 changes: 90 additions & 23 deletions Sources/SwiftBin/SwiftBin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ public macro BinaryFormat() = #externalMacro(module: "SwiftBinMacros", type: "Bi
public macro BinaryEnum() = #externalMacro(module: "SwiftBinMacros", type: "BinaryEnumMacro")

@attached(member, names: named(init), named(serialize), named(Marker))
@attached(extension, conformances: BinaryNonFrozenEnumProtocol)
@attached(extension, conformances: NonFrozenBinaryEnumProtocol)
public macro OpenBinaryEnum() = #externalMacro(module: "SwiftBinMacros", type: "BinaryEnumMacro")

public enum BinarySerializationError: Error {
case lengthDoesNotFit
}

public enum BinaryParsingError: Error {
case invalidOrUnknownEnumValue
case invalidOrUnknownEnumValue, invalidUTF8
}

public struct BinaryBuffer: ~Copyable {
Expand Down Expand Up @@ -199,41 +199,32 @@ public protocol BinaryEnumProtocol: BinaryFormatProtocol {
// associatedtype Marker: RawRepresentable where Marker.RawValue: FixedWidthInteger
}

public protocol BinaryNonFrozenEnumProtocol: BinaryEnumProtocol {
public protocol NonFrozenBinaryEnumProtocol: BinaryEnumProtocol {
static var unknown: Self { get }
}

public protocol BinaryFormatWithLength: BinaryFormatProtocol {
var byteSize: Int { get }
}

@propertyWrapper public struct LengthEncoded<Length: FixedWidthInteger, Value: BinaryFormatWithLength>: BinaryFormatProtocol {
public var wrappedValue: Value
public var projectedValue: Self { self }
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
init(consumingWithoutLength buffer: inout BinaryBuffer) throws
func serializeWithoutLength(into writer: inout BinaryWriter) throws
}

extension BinaryFormatWithLength {
public init(consuming buffer: inout BinaryBuffer) throws {
let length = try Int(buffer.readInteger(Length.self))
let length = try Int(buffer.readInteger(Int.self))
guard buffer.count >= length else {
throw BinaryParsingNeedsMoreDataError()
}

self.wrappedValue = try buffer.readWithBuffer(length: length) { buffer in
try Value(consuming: &buffer)
self = try buffer.readWithBuffer(length: length) { buffer in
try Self(consumingWithoutLength: &buffer)
}
}

public func serialize(into writer: inout BinaryWriter) throws {
let byteSize = wrappedValue.byteSize

guard byteSize <= Length.max else {
throw BinarySerializationError.lengthDoesNotFit
}

try writer.writeInteger(Length(byteSize))
try wrappedValue.serialize(into: &writer)
try writer.writeInteger(byteSize)
try serializeWithoutLength(into: &writer)
}
}

Expand Down Expand Up @@ -293,11 +284,41 @@ extension Float: BinaryFormatProtocol {
}
}

extension Array: BinaryFormatProtocol where Element: BinaryFormatProtocol {
public init(consuming buffer: inout BinaryBuffer) throws {
let count: Int = try buffer.readInteger()
var elements = [Element]()
for _ in 0..<count {
elements.append(try Element(consuming: &buffer))
}
self = elements
}

public func serialize(into writer: inout BinaryWriter) throws {
try writer.writeInteger(count)
for element in self {
try element.serialize(into: &writer)
}
}
}

extension String: BinaryFormatWithLength {
public var byteSize: Int { utf8.count }
public func serializeWithoutLength(into writer: inout BinaryWriter) throws {
try writer.writeString(self)
}

public init(consumingWithoutLength buffer: inout BinaryBuffer) throws {
self = try buffer.readString(length: buffer.count)
}
}

#if canImport(Foundation)
import Foundation

extension Data: BinaryFormatWithLength {
public var byteSize: Int { count }
public func serialize(into writer: inout BinaryWriter) throws {
public func serializeWithoutLength(into writer: inout BinaryWriter) throws {
try withUnsafeBytes { buffer in
try writer.writeBytes(
buffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
Expand All @@ -306,9 +327,55 @@ extension Data: BinaryFormatWithLength {
}
}

public init(consuming buffer: inout BinaryBuffer) throws {
public init(consumingWithoutLength buffer: inout BinaryBuffer) throws {
self = buffer.withConsumedBuffer { buffer in
Data(bytes: buffer.baseAddress!, count: buffer.count)
}
}
}

extension BinaryBuffer {
public static func readContents<T>(
ofData data: Data,
parse: (inout BinaryBuffer) throws -> T
) rethrows -> T {
try data.withUnsafeBytes { buffer in
let buffer = buffer.bindMemory(to: UInt8.self)
var binary = BinaryBuffer(
pointer: buffer.baseAddress!,
count: buffer.count,
release: nil
)

return try parse(&binary)
}
}
}

extension BinaryFormatProtocol {
public init(parseFrom data: Data) throws {
self = try BinaryBuffer.readContents(ofData: data) { buffer in
try Self(consuming: &buffer)
}
}

public func writeData() throws -> Data {
var data = Data()
data.reserveCapacity(4096)
try write(into: &data)
return data
}

public func write(into data: inout Data) throws {
var writeBuffer = data
var writer = BinaryWriter(
defaultEndianness: .big
) { dataToWrite in
writeBuffer.append(dataToWrite.bindMemory(to: UInt8.self))
}
try serialize(into: &writer)

data = writeBuffer
}
}
#endif
6 changes: 3 additions & 3 deletions Sources/SwiftBinMacros/SwiftBinMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public struct BinaryEnumMacro: MemberMacro, ExtensionMacro {
case .notAnEnum:
return "Type is not an enum"
case .unsupportedEnumCasesCount:
return "BinaryEnum supports only \(UInt8.max) cases per enum"
return "BinaryEnum supports only \(UInt16.max) cases per enum"
}
}
}
Expand Down Expand Up @@ -96,7 +96,7 @@ public struct BinaryEnumMacro: MemberMacro, ExtensionMacro {
}
}

if enumCases.count > Int(UInt8.max) {
if enumCases.count > Int(UInt16.max) {
throw Error.unsupportedEnumCasesCount
}

Expand Down Expand Up @@ -153,7 +153,7 @@ public struct BinaryEnumMacro: MemberMacro, ExtensionMacro {

return [
"""
public enum Marker: UInt8, Hashable, BinaryFormatProtocol {
public enum Marker: UInt16, Hashable, BinaryFormatProtocol {
\(raw: markerCases.joined(separator: "\n"))
}
""",
Expand Down

0 comments on commit 7871588

Please sign in to comment.