From abfa06ceb59175b7de11b3fe2686737ee6ec6a01 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 31 Mar 2024 23:35:13 +0200 Subject: [PATCH] Initial commit --- .gitignore | 8 + Package.resolved | 15 ++ Package.swift | 41 ++++ Sources/SwiftBin/SwiftBin.swift | 262 +++++++++++++++++++++ Sources/SwiftBinMacros/SwiftBinMacro.swift | 214 +++++++++++++++++ Tests/SwiftBinTests/SwiftBinTests.swift | 12 + 6 files changed, 552 insertions(+) create mode 100644 .gitignore create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 Sources/SwiftBin/SwiftBin.swift create mode 100644 Sources/SwiftBinMacros/SwiftBinMacro.swift create mode 100644 Tests/SwiftBinTests/SwiftBinTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..387800b --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "c8b42ad06f220997e6d7b3ee99605e794ea71f5ca77f24e6421f398e385119ae", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..1c63edb --- /dev/null +++ b/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +import CompilerPluginSupport + +let package = Package( + name: "SwiftBin", + platforms: [ + .macOS(.v10_15), + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "SwiftBin", + targets: ["SwiftBin"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.0-latest"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .macro( + name: "SwiftBinMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), + .target( + name: "SwiftBin", + dependencies: ["SwiftBinMacros"] + ), + .testTarget( + name: "SwiftBinTests", + dependencies: ["SwiftBin"] + ), + ] +) diff --git a/Sources/SwiftBin/SwiftBin.swift b/Sources/SwiftBin/SwiftBin.swift new file mode 100644 index 0000000..cbc9e6d --- /dev/null +++ b/Sources/SwiftBin/SwiftBin.swift @@ -0,0 +1,262 @@ +public struct BinaryParsingNeedsMoreDataError: Error {} + +@attached(member, names: named(init), named(serialize)) +@attached(extension, conformances: BinaryFormatProtocol) +public macro BinaryFormat() = #externalMacro(module: "SwiftBinMacros", type: "BinaryFormatMacro") + +@attached(member, names: named(init), named(serialize), named(Marker)) +@attached(extension, conformances: BinaryEnumProtocol) +public macro BinaryEnum() = #externalMacro(module: "SwiftBinMacros", type: "BinaryEnumMacro") + +@attached(member, names: named(init), named(serialize), named(Marker)) +@attached(extension, conformances: BinaryNonFrozenEnumProtocol) +public macro OpenBinaryEnum() = #externalMacro(module: "SwiftBinMacros", type: "BinaryEnumMacro") + +public enum BinarySerializationError: Error { + case lengthDoesNotFit +} + +public enum BinaryParsingError: Error { + case invalidOrUnknownEnumValue +} + +public struct BinaryBuffer: ~Copyable { + internal var pointer: UnsafePointer + internal var count: Int + private let release: (() -> Void)? + + public typealias ReleaseCallback = () -> Void + + public init(pointer: UnsafePointer, count: Int, release: ReleaseCallback?) { + self.pointer = pointer + self.count = count + self.release = release + } + + private mutating func advance(by length: Int) { + pointer += length + count -= length + } + + internal mutating func readInteger(_ type: F.Type = F.self) throws -> F { + let size = MemoryLayout.size + if count < size { + throw BinaryParsingNeedsMoreDataError() + } + + let value = pointer.withMemoryRebound(to: F.self, capacity: 1) { $0.pointee } + advance(by: size) + return value + } + + internal mutating func readWithBuffer(length: Int, parse: (inout BinaryBuffer) throws -> T) throws -> T { + guard count >= length else { + throw BinaryParsingNeedsMoreDataError() + } + + var buffer = BinaryBuffer(pointer: pointer, count: length, release: nil) + let value = try parse(&buffer) + advance(by: length) + return value + } + + internal mutating func readString(length: Int) throws -> String { + try readWithBuffer(length: length) { buffer in + buffer.getString() + } + } + + internal mutating func withConsumedBuffer( + parse: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + let value = try parse(UnsafeBufferPointer(start: pointer, count: count)) + advance(by: count) + return value + } + + internal mutating func getString() -> String { + withConsumedBuffer { buffer in + String(decoding: buffer, as: UTF8.self) + } + } + + deinit { release?() } +} + +public enum Endianness { + case little, big + + @inlinable + internal func convert(_ integer: F) -> F { + switch self { + case .little: return integer.littleEndian + case .big: return integer.bigEndian + } + } +} + +public struct BinaryWriter: ~Copyable { + public typealias WriteCallback = (UnsafeRawBufferPointer) throws -> Void + + public var defaultEndianness: Endianness + + @usableFromInline + internal let write: WriteCallback + + public init(defaultEndianness: Endianness, write: @escaping WriteCallback) { + self.defaultEndianness = defaultEndianness + self.write = write + } + + @inlinable + public mutating func writeInteger(_ integer: F, endianness: Endianness? = nil) throws { + let endianness = endianness ?? defaultEndianness + let converted = endianness.convert(integer) + try withUnsafeBytes(of: converted, write) + } + + @inlinable + public mutating func writeBytes(_ pointer: UnsafePointer, size: Int) throws { + try write(UnsafeRawBufferPointer(start: pointer, count: size)) + } + + @inlinable + public mutating func writeString(_ string: String) throws { + try writeBytes(string, size: string.utf8.count) + } +} + +public enum BinaryParsingResult { + case parsed(T) + case needsMoreData + + public func map(_ map: (T) -> N) -> BinaryParsingResult { + switch self { + case .parsed(let value): return .parsed(map(value)) + case .needsMoreData: return .needsMoreData + } + } +} + +public protocol BinaryFormatProtocol { + init(consuming buffer: inout BinaryBuffer) throws + func serialize(into writer: inout BinaryWriter) throws +} + +public protocol BinaryEnumProtocol: BinaryFormatProtocol { +// associatedtype Marker: RawRepresentable where Marker.RawValue: FixedWidthInteger +} + +public protocol BinaryNonFrozenEnumProtocol: BinaryEnumProtocol { + static var unknown: Self { get } +} + +public protocol BinaryFormatWithLength: BinaryFormatProtocol { + var byteSize: Int { get } +} + +@propertyWrapper public struct LengthEncoded: BinaryFormatProtocol { + public var wrappedValue: Value + public var projectedValue: Self { self } + public init(wrappedValue: Value) { + self.wrappedValue = wrappedValue + } + + public init(consuming buffer: inout BinaryBuffer) throws { + let length = try Int(buffer.readInteger(Length.self)) + guard buffer.count >= length else { + throw BinaryParsingNeedsMoreDataError() + } + + self.wrappedValue = try buffer.readWithBuffer(length: length) { buffer in + try Value(consuming: &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) + } +} + +extension FixedWidthInteger where Self: BinaryFormatProtocol { + public func serialize(into writer: inout BinaryWriter) throws { + try writer.writeInteger(self) + } + + public init(consuming buffer: inout BinaryBuffer) throws { + self = try buffer.readInteger() + } +} + +extension Int: BinaryFormatProtocol {} +extension Int8: BinaryFormatProtocol {} +extension Int16: BinaryFormatProtocol {} +extension Int32: BinaryFormatProtocol {} +extension Int64: BinaryFormatProtocol {} +extension UInt: BinaryFormatProtocol {} +extension UInt8: BinaryFormatProtocol {} +extension UInt16: BinaryFormatProtocol {} +extension UInt32: BinaryFormatProtocol {} +extension UInt64: BinaryFormatProtocol {} + +extension RawRepresentable where Self: BinaryFormatProtocol, RawValue: FixedWidthInteger & BinaryFormatProtocol { + public func serialize(into writer: inout BinaryWriter) throws { + try writer.writeInteger(rawValue) + } + + public init(consuming buffer: inout BinaryBuffer) throws { + let number = try buffer.readInteger(RawValue.self) + guard let value = Self(rawValue: number) else { + throw BinaryParsingError.invalidOrUnknownEnumValue + } + + self = value + } +} + +extension Double: BinaryFormatProtocol { + public func serialize(into writer: inout BinaryWriter) throws { + try writer.writeInteger(bitPattern) + } + + public init(consuming buffer: inout BinaryBuffer) throws { + try self.init(bitPattern: UInt64(consuming: &buffer)) + } +} + +extension Float: BinaryFormatProtocol { + public func serialize(into writer: inout BinaryWriter) throws { + try writer.writeInteger(bitPattern) + } + + public init(consuming buffer: inout BinaryBuffer) throws { + try self.init(bitPattern: UInt32(consuming: &buffer)) + } +} + +import Foundation + +extension Data: BinaryFormatWithLength { + public var byteSize: Int { count } + public func serialize(into writer: inout BinaryWriter) throws { + try withUnsafeBytes { buffer in + try writer.writeBytes( + buffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + size: buffer.count + ) + } + } + + public init(consuming buffer: inout BinaryBuffer) throws { + self = buffer.withConsumedBuffer { buffer in + Data(bytes: buffer.baseAddress!, count: buffer.count) + } + } +} diff --git a/Sources/SwiftBinMacros/SwiftBinMacro.swift b/Sources/SwiftBinMacros/SwiftBinMacro.swift new file mode 100644 index 0000000..fa48fdc --- /dev/null +++ b/Sources/SwiftBinMacros/SwiftBinMacro.swift @@ -0,0 +1,214 @@ +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +struct EnumCase { + let name: String + let members: [String?] + var memberCount: Int { members.count } + + var parameters: [String] { + (1...memberCount).map { index in + "c\(index)" + } + } + + var labels: [String] { + members.map { member in + if let member { + return "\(member): " + } else { + return "" + } + } + } + + var captureParameters: String { + if memberCount == 0 { return "" } + let captures = parameters.map { "let \($0)" } + return "(\(captures.joined(separator: ", ")))" + } + + var captureParametersSerializeStatements: [String] { + if memberCount == 0 { return [] } + return parameters.map { "try \($0).serialize(into: &writer)" } + } + + var parseParameters: String { + if memberCount == 0 { return "" } + let captures = labels.map { label in + return "\(label)try .init(consuming: &buffer)" + } + return "(\(captures.joined(separator: ", ")))" + } +} + +public struct BinaryEnumMacro: MemberMacro, ExtensionMacro { + enum Error: Swift.Error, CustomDebugStringConvertible { + case notAnEnum, unsupportedEnumCasesCount + + var debugDescription: String { + switch self { + case .notAnEnum: + return "Type is not an enum" + case .unsupportedEnumCasesCount: + return "BinaryEnum supports only \(UInt8.max) cases per enum" + } + } + } + + public static var formatMode: FormatMode { .auto } + + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard declaration.is(EnumDeclSyntax.self) else { + throw Error.notAnEnum + } + + let memberList = declaration.memberBlock.members + + let enumCases = memberList.flatMap { member -> [EnumCase] in + guard + let enumCase = member + .decl + .as(EnumCaseDeclSyntax.self) + else { + return [] + } + + return enumCase.elements.map { element in + guard let parameterClause = element.parameterClause else { + return EnumCase( + name: element.name.text, + members: [] + ) + } + + return EnumCase( + name: element.name.text, + members: parameterClause.parameters.map(\.firstName?.text) + ) + } + } + + if enumCases.count > Int(UInt8.max) { + throw Error.unsupportedEnumCasesCount + } + + let markerCases = enumCases.enumerated().map { (index, enumCase) in + "case \(enumCase.name) = \(index)" + } + + let parseStatements = enumCases.map { enumCase in + return """ + case .\(enumCase.name): + self = .\(enumCase.name)\(enumCase.parseParameters) + """ + } + + let serializeStatements = enumCases.map { enumCase in + """ + case .\(enumCase.name)\(enumCase.captureParameters): + try Marker.\(enumCase.name).serialize(into: &writer) + \(enumCase.captureParametersSerializeStatements.joined(separator: "\n")) + """ + } + + return [ + """ + public enum Marker: UInt8, Hashable, BinaryFormatProtocol { + \(raw: markerCases.joined(separator: "\n")) + } + """, + """ + init(consuming buffer: inout BinaryBuffer) throws { + switch try Marker(consuming: &buffer) { + \(raw: parseStatements.joined(separator: "\n")) + } + } + """, + """ + func serialize(into writer: inout BinaryWriter) throws { + switch self { + \(raw: serializeStatements.joined(separator: "\n")) + } + } + """ + ] + } + + public static func expansion(of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { + [] + } +} + +public struct BinaryFormatMacro: MemberMacro, ExtensionMacro { + public static var formatMode: FormatMode { .auto } + + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + let memberList = declaration.memberBlock.members + + let properties = memberList.compactMap { member -> String? in + guard + let propertyName = member + .decl + .as(VariableDeclSyntax.self)? + .bindings + .first? + .pattern + .as(IdentifierPatternSyntax.self)? + .identifier + .text + else { + return nil + } + + if member.decl.as(VariableDeclSyntax.self)?.attributes.isEmpty == false { + return "_" + propertyName + } else { + return propertyName + } + } + + let parseStatements = properties.map { property in + "self.\(property) = try .init(consuming: &buffer)" + } + + let serializeStatements = properties.map { property in + "try self.\(property).serialize(into: &writer)" + } + + return [ + """ + init(consuming buffer: inout BinaryBuffer) throws { + \(raw: parseStatements.joined(separator: "\n")) + } + """, + """ + func serialize(into writer: inout BinaryWriter) throws { + \(raw: serializeStatements.joined(separator: "\n")) + } + """ + ] + } + + public static func expansion(of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { + [] + } +} + +@main +struct testPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + BinaryFormatMacro.self, + BinaryEnumMacro.self, + ] +} diff --git a/Tests/SwiftBinTests/SwiftBinTests.swift b/Tests/SwiftBinTests/SwiftBinTests.swift new file mode 100644 index 0000000..52d9d62 --- /dev/null +++ b/Tests/SwiftBinTests/SwiftBinTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import SwiftBin + +final class SwiftBinTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +}