07b9183

Bowmap · bowmap.git · Tickets

Branches > main > 07b9183 > files / deps.edn

Decouple frames from mutable, writable buffers

Grahp, 2026-09-29

Commit: 07b9183626cca706abdf710342b25c11005b3b6b
Parent: 2258a02

5 changed files:

src/net/bowmap/server/network/streams.clj modified (view file)

@@ -1,10 +1,9 @@ (ns net.bowmap.server.network.streams   "Functions for reading and writing frames to Input and OutputStreams"   (:require-   [net.bowmap.server.protocol.frame :refer [frame]]+   [net.bowmap.server.protocol.frame :as frame]    [net.bowmap.server.protocol.types :refer [max-varint-size]])   (:import-   [io.netty.buffer ByteBuf]    [java.io     EOFException     IOException@@ -46,7 +45,7 @@           (if (= r -1)             (throw (EOFException. "EOF while reading frame"))             (recur (+ offset r))))))-    (frame arr)))+    (frame/frame arr)))  (defn write-varint!!   "Blocks and writes a varint to an OutputStream."@@ -63,9 +62,8 @@   "Blocks and writes a frame to an OutputStream.    Throws if the frame cannot be written."   [frame output-stream]-  (let [len (ByteBuf/.readableBytes frame)-        arr (byte-array len)]-    (ByteBuf/.readBytes frame arr)-    (write-varint!! output-stream len)-    (OutputStream/.write output-stream arr)-    (OutputStream/.flush output-stream)))+  (frame/write! frame+    (fn [array length]+        (write-varint!! output-stream length)+        (OutputStream/.write output-stream array 0 length)+        (OutputStream/.flush output-stream))))

src/net/bowmap/server/protocol/buffer.clj added (view file)

@@ -0,0 +1,24 @@+(ns net.bowmap.server.protocol.buffer+  (:require+   [clojure.spec.alpha :as s]+   [net.bowmap.server.protocol.frame :as frame])+  (:import+   [java.io ByteArrayOutputStream]))++(defprotocol WriteBuffer+  "A single-threaded transiently mutable growable buffer of bytes."+  (write-byte! [buf b] "Returns this buffer after adding one byte to the end.+                        Byte may be signed or unsigned.")+  (->frame [buf] "Returns a frame from this buf.+                  After this call, the buffer is not to be mutated."))++(s/def ::write-buffer #(satisfies? WriteBuffer %))++(defn write-buffer []+  (let [baos (ByteArrayOutputStream.)]+    (reify WriteBuffer+      (write-byte! [buf b]+        (^[int] ByteArrayOutputStream/.write baos (bit-and 0xFF b))+        buf)+      (->frame [_]+        (frame/frame (ByteArrayOutputStream/.toByteArray baos))))))

src/net/bowmap/server/protocol/frame.clj modified (view file)

@@ -1,12 +1,33 @@ (ns net.bowmap.server.protocol.frame-  "A framed byte buffer containing one serialized Minecraft protocol packet.-   A frame contains the packet's numeric id and payload.-   Frames are not length-prefixed.-   See https://minecraft.wiki/w/Java_Edition_protocol/Packets for the format."+  (:require+   [clojure.spec.alpha :as s])   (:import-   [io.netty.buffer Unpooled]))+   [io.netty.buffer ByteBuf Unpooled]))++(defprotocol Frame+  "A immutable sequence of bytes representing one framed, serialized Minecraft protocol packet.+   Frames contain the packet's numeric id and payload, but are not length-prefixed.+   See https://minecraft.wiki/w/Java_Edition_protocol/Packets for the packet format."+  (write! [frame f] "Calls 'f' with the array and length of the frame."))++(s/def ::frame #(satisfies? Frame %))  (defn frame-  "Returns an empty frame, or a frame containing the bytes of an array."-  ([] (Unpooled/buffer))-  ([arr] (Unpooled/wrappedBuffer arr)))+  ([array] (frame array (alength array)))+  ([array length]+   ^{:type ::frame}+   (reify Frame+     (write! [_ f] (f array length)))))++(defmethod print-method ::frame [frame writer]+  (write! frame+          (fn [array length]+            (.write writer "#Frame ")+            (.write writer (pr-str (subvec (vec array) 0 length))))))++(defn ->ByteBuf+  "Returns a read-only ByteBuf containing the bytes of a frame."+  [frame]+  (write! frame+          (fn [array length]+            (ByteBuf/.asReadOnly (Unpooled/wrappedBuffer array 0 length)))))

src/net/bowmap/server/protocol/packet_serialization.clj modified (view file)

@@ -1,7 +1,8 @@ (ns net.bowmap.server.protocol.packet-serialization   (:require+   [net.bowmap.server.protocol.buffer :as buffer]    [net.bowmap.server.protocol.clientbound-packets :as clientbound]-   [net.bowmap.server.protocol.frame :refer [frame]]+   [net.bowmap.server.protocol.frame :as frame]    [net.bowmap.server.protocol.packet :as packet]    [net.bowmap.server.protocol.phase :as phase]    [net.bowmap.server.protocol.readers :as readers]@@ -14,16 +15,18 @@   [{::phase/keys! [phase]} {::packet/keys! [id] :as packet}]   (let [numeric-id (req! (clientbound/phase->id->numeric-id phase) id)         writer (req! (clientbound/phase->id->writer phase) id)]-    (-> (frame)-        (writers/varint> numeric-id)-        (writer packet))))+    (buffer/->frame+      (-> (buffer/write-buffer)+          (writers/varint> numeric-id)+          (writer packet)))))  (defn decode-frame   "Decodes a packet from a frame.    Can throw if the frame does not contain a valid packet."   [{:keys! [::phase/phase]} frame]-  (let [numeric-id (readers/varint< frame)+  (let [buf (frame/->ByteBuf frame)+        numeric-id (readers/varint< buf)         id (req! (serverbound/phase->numeric-id->id phase) numeric-id)         reader (req! (serverbound/phase->id->reader phase) id)]-    (assoc (reader frame)+    (assoc (reader buf)            ::packet/id id)))

src/net/bowmap/server/protocol/writers.clj modified (view file)

@@ -1,18 +1,19 @@ (ns net.bowmap.server.protocol.writers-  "Functions for writing values into io.netty.ByteBufs.+  "Functions for writing values into write buffers.    Functions look like type>, where 'type' is a Minecraft protocol type.    See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types"   (:require    [clojure.data.json :as json]    [clojure.spec.alpha :as s]+   [net.bowmap.server.protocol.buffer :as buffer]    [net.bowmap.server.protocol.schema :as p])   (:import-   [io.netty.buffer ByteBuf]    [java.nio.charset StandardCharsets]    [java.util UUID]))  (s/def ::writer (s/fspec-                  :args (s/cat :buf #(instance? ByteBuf %) :value any?)+                  :args (s/cat :buf ::buffer/write-buffer+                               :value any?)                   :fn #(= (-> % :args :buf)                           (:ret %)))) @@ -25,6 +26,7 @@     (catch Throwable t       (throw (ex-info "Error while writing table field" {:field field                                                          field (get m field)+                                                         :writer writer                                                          :table m}                       t))))) @@ -51,14 +53,28 @@     (fn? x) x     :else (throw (IllegalArgumentException. (str "Cannot coerce into writer: " (pr-str x)))))) -(def byte> ByteBuf/.writeByte)-(def ubyte> ByteBuf/.writeByte)-(def short> ByteBuf/.writeShort)-(def ushort> ByteBuf/.writeShort)-(def int> ByteBuf/.writeInt)-(def long> ByteBuf/.writeLong)-(def float> ByteBuf/.writeFloat)-(def double> ByteBuf/.writeDouble)+(def byte> buffer/write-byte!)++(defn- integer> [byte-count]+  (let [range (range (* 8 (dec byte-count)) -1 -8)]+    (fn [buf n]+      (reduce (fn [acc bits]+                (byte> acc (bit-shift-right n bits)))+              buf range))))++(defn- apply>+  "Returns a writer that writes (f x) via 'writer'."+  [f writer]+  (fn [buf x]+    (writer buf (f x))))++(def ubyte> byte>)+(def short> (integer> 2))+(def ushort> short>)+(def int> (integer> 4))+(def long> (integer> 8))+(def float> (apply> Float/floatToRawIntBits int>))+(def double> (apply> Double/doubleToRawLongBits long>))  (defn boolean>   "Writes the byte 1 if 'x' is truthy, else the byte 0."@@ -77,44 +93,6 @@         (recur (byte> buf (bit-or b 0x80))                nxt))))) -(defn string>-  "Writes a string as a varint byte length followed by its UTF-8 bytes.--   When passed 1 arg, returns  a string writer that throws when the string-   exceeds 'max-length' characters."-  ([max-length]-   (fn [buf s]-     (if (> (count s) max-length)-       (throw (ex-info "Cannot encode string, too long" {:max-length max-length :string s}))-       (string> buf s))))-  ([buf s]-   {:pre [(string? s)]}-   (doto buf-     (varint> (count s))-     (ByteBuf/.writeCharSequence s StandardCharsets/UTF_8))))--(defn json>-  "Turns 'x' into JSON and writes it as a string."-  [buf x]-  (string> buf (json/write-str x)))--(defn status-response> [buf {::p/keys! [version-name protocol]-                              ::p/keys [motd max-player-count online-player-count]}]-  (json> buf-         (cond-> {:version {:name version-name-                            :protocol protocol}}-           motd (assoc :description motd)--           (and max-player-count online-player-count)-           (assoc :players {:max max-player-count-                            :online online-player-count}))))--(defn uuid> [buf uuid]-  {:pre [(uuid? uuid)]}-  (-> buf-      (long> (UUID/.getMostSignificantBits uuid))-      (long> (UUID/.getLeastSignificantBits uuid))))- (defn array>   "Returns a writer that writes every value in a coll via 'writer'."   [writer]@@ -133,8 +111,45 @@       (-> (varint> buf (count xs))           (array-writer xs))))) +(def bytes> (list> byte>))++(defn string>+  "Writes a string as a varint byte length followed by its UTF-8 bytes.++   When passed 1 arg, returns a string writer that throws when the string+   exceeds 'max-length' characters."+  ([max-length]+   (fn [buf s]+     (if (> (count s) max-length)+       (throw (ex-info "Cannot encode string, too long" {:max-length max-length :string s}))+       (string> buf s))))+  ([buf s]+   (bytes> buf (String/.getBytes s StandardCharsets/UTF_8))))++(def json>+  "Turns 'x' into JSON and writes it as a string."+  (apply> json/write-str string>))++(defn- ->status-response [{::p/keys! [version-name protocol]+                           ::p/keys [motd max-player-count online-player-count]}]+  (cond-> {:version {:name version-name+                     :protocol protocol}}+    motd (assoc :description motd)++    (and max-player-count online-player-count)+    (assoc :players {:max max-player-count+                     :online online-player-count})))++(def status-response> (apply> ->status-response json>))++(defn uuid> [buf uuid]+  {:pre [(uuid? uuid)]}+  (-> buf+      (long> (UUID/.getMostSignificantBits uuid))+      (long> (UUID/.getLeastSignificantBits uuid))))+ (defn context-optional>-  "Returns a writer that writes a value via 'writer' if the value is non-nil."+  "Returns a writer that only writes a value via 'writer' if the value is non-nil."   [writer]   (fn [buf x]     (if (some? x)@@ -143,7 +158,7 @@  (defn optional>   "Returns a writer that prefixes the value with a boolean indicating whether-   it is present, and writes the value via 'writer' if it is non-nil."+   it is present, and only writes the value via 'writer' if the value is non-nil."   [writer]   (let [writer (context-optional> writer)]     (fn [buf x]