I have a library for encoding and decoding types in a private binary format. The library is very simple and is meant to be fast.
Focusing only on the encoding (the issue is the same for decoding), it follows the classic scheme in which a type adopts the protocol:
public protocol BEncodable {
func encode(to encoder: inout some BEncoder) throws
}
and receive a BEncoder
istance:
public protocol BEncoder {
var userVersion: UInt32 { get }
var userData: Any? { get }
mutating func encode<Value> (_ value: Value ) throws where Value : BEncodable
}
to encode its fields, like in this example from my package:
extension CGSize : BEncodable {
public func encode(to encoder: inout some BEncoder) throws {
try encoder.encode( width )
try encoder.encode( height )
}
}
The type that implements the BEncoder
protocol is a struct called BinaryIOEncoder
(a class makes the code slower by 10-20%, but this is not the subject of the question) and contains the functions necessary to encode the "primitive" types, which in this case are Bool
, all integer types, Float
, Double
, String
, Data
.
And so a type adopting BEncodable
encodes its fields, which in turn encode their fields, and so on, until a primitive type is reached, say UInt16
:
extension UInt16: BEncodable {
public func encode(to encoder: inout some BEncoder) throws {
/* ... */
}
}
The encode(...)
method of UInt16
therefore needs to call the primitive function in the BinaryIOEncoder
struct which adopts the BEncoder
protocol and effectively writes the value in the data buffer.
So I have two possibilities.
The first is to include in the BEncoder
protocol the BinaryIOEncoder
methods for encoding primitive types, which, once they become part of a public protocol, also become public:
public protocol BEncoder {
var userVersion: UInt32 { get }
var userData: Any? { get }
mutating func encode<Value> (_ value: Value ) throws where Value : BEncodable
// Everything that follows should be an internal detail:
mutating func encodeBool( _ value:Bool ) throws
mutating func encodeUInt8( _ value:UInt8 ) throws
mutating func encodeUInt16( _ value:UInt16 ) throws
mutating func encodeUInt32( _ value:UInt32 ) throws
mutating func encodeUInt64( _ value:UInt64 )
mutating func encodeUInt( _ value:UInt ) throws
mutating func encodeInt8( _ value:Int8 ) throws
mutating func encodeInt16( _ value:Int16 ) throws
mutating func encodeInt32( _ value:Int32 ) throws
mutating func encodeInt64( _ value:Int64 ) throw
mutating func encodeInt( _ value:Int ) throws
mutating func encodeFloat( _ value:Float ) throws
mutating func encodeDouble( _ value:Double ) throws
mutating func encodeString<T>( _ value:T ) throws
where T:StringProtocol
mutating func encodeData<T>( _ value:T ) throws
where T:MutableDataProtocol
}
and then:
extension UInt16: BEncodable {
public func encode(to encoder: inout some BEncoder) throws {
try encoder.encodeUInt16( self )
}
}
But I don't really want to make public all these methods which are an implementation detail , so the second possibility is not to change at all the BEncoder
protocol but writing for each primitive type this sort of horrendous "unsafe cast":
extension UInt16: BEncodable {
public func encode(to encoder: inout some BEncoder) throws {
try withUnsafeMutablePointer(to: &encoder) {
try $0.withMemoryRebound(to: BinaryIOEncoder.self, capacity: 1) {
try $0.pointee.encodeUInt16( self )
}
}
}
}
I don't even know how reliable it is.
Do I miss the third, fourth, …, nth possibility? How can I design this public interface and work around the fact that I can't declare internal methods in a public protocol?
Thank you in advance for any suggestion.
Post scriptum: I know, I can get rid of BEncoder
and pass directly BinaryIOEncoder
as encode(to:...)
parameter:
public protocol BEncodable {
func encode(to encoder: inout BinaryIOEncoder) throws
}
I can then set up how I want visibility.
This was my first design but there are reasons why I would like to avoid doing it.