
So far, I’ve made two posts creating an HTTP client which sends and receives protocol buffer messages and an HTTP Server that accepts and respond with protocol buffer messages. In both of these posts we had to do a lot of extra toil in serializing protocol buffers into base64-encoded strings and deserialize protocol buffers from base64-encoded strings. In this post we create three functions and a macro to help us serialize and deserialize protocol buffers in our http server and client.
Notes:
I will be discussing the Hello World Server and Hello World Client. If you missed those blog posts it may be useful to go and view them here and here. There has been code drift since those posts, mainly the changes we will discuss in this post. The source code for the utility functions can be found in my protobuf-utilities code repo on github.
Code Discussion
This time we will omit the discussion of the asd files. We went through the asd files line-by-line in the two posts referenced in the notes so please look at those.
In addition to the main macros we discuss and show below, we use two helper functions deserialize-proto-from-base64-string and serialize-proto-to-base64-string which can be found in my protobuf-utilities repo.
Server-Side
We noticed a large part of the problem with using cl-protobufs protocol buffer objects in an HTTP request and response is the tedium of translating from the base64-encoded string that was sent into the server to a protocol buffer and then reversing the protocol buffer with the response object. We know which parameters to our HTTP handler will be either nil or a base64-encoded proto packed in a string and their respective types. With this we can make a macro to translate the strings to their respective protos and use them in an enclosing lexical scope.
Why a macro? Many Lispers may not ask this question, but we should as macros are harder to reason about than functions. We want the body of our macro to run in a scope where it has access to all of the deserialized protobuf messages. We are creating a utility that will work for all lists of proto messages so long as we know their types. We could with effort and ugliness make a function that accepts a function, and have that outer function funcall the inner function, but it would be ugly. With a macro we can create new syntax which will simplify code, allowing us to simply list the protobuf messages we wish to deserialize and then use them.
Given that, what our macro should accept is obvious, a list of conses each containing the variable that holds an encoded proto and the type of message to be encoded/decoded. We also take a body in which the supplied symbols will now refer to a deserialized proto.
(defmacro with-deserialized-protos
(message-message-type-list &body body)
"Take a list (MESSAGE . PROTO-TYPE)
MESSAGE-MESSAGE-TYPE-LIST where the message will be
a symbol pointing to a base64-encoded serialized proto
in a string. Deserialize the protos and store them in
the message symbols. The messages are bound lexically
so after this macro finishes the protos return to be
serialized base64-encoded strings."
`(let ,(loop for (message . message-type)
in message-message-type-list
collect
`(,message
(deserialize-proto-from-base64-string
',message-type
(or ,message ""))))
,@body))
It is plausible that our HTTP server will respond with a base64-encoded protocol buffer object. We could first call `with-deserialized-protos` to do some processing, creating a new protocol buffer object, and then call a function like `serialize-proto-to-base64-string`. Instead I create a macro that will automatically serialize to string then base64-encode the result of a body.
(defmacro serialize-result (&body body)
(let ((result-proto (gensym "RESULT-PROTO")))
`(let ((,result-proto ,@body))
(serialize-proto-to-base64-string ,result-proto))))
Since we’ve gone this far, we can string these two macros together:
(defmacro with-deserialized-protos-serializing-return
(message-message-type-list &body body)
`(serialize-result (with-deserialized-protos
,message-message-type-list ,@body)))
This vastly improves our handler:
(define-easy-handler (hello-world :uri "/hello")
((request :parameter-type 'string))
(pu:with-deserialized-protos-serializing-return
((request . hwp:request))
(hwp:make-response
:response
(if (hwp:request.has-name request)
(format nil "Hello ~a" (hwp:request.name request))
"Hello"))))
A final pro-macro argument: Macros allow us to make syntax that describes what we want a region of code to accomplish. The macros I wrote aren’t distinctly necessary, you could just call `deserialize-proto-from-base64-string` several times in a let binding. Since you probably only have one request proto that would do find. You could also deserialize the return proto yourself. I find the written macros makes the code nicer to write, the downside is people working on the code will have to know what these macros do. Thankfully, we have M-x and docstrings for that.
Client-Side
We have the reverse story on the client side. We start by having to serialize and base64-encode our proto object before sending them over the wire, and then deserialize the result. One would imagine writing the same kind of macro here as we wrote on the server side. The problem with that is there’s no real body we want to run with our serialized protos we want to send over the wire, and we get one proto back so we can just serialize the HTTP result proto object and let bind it. We can just use a function for this.
(defun proto-call
(call-name-proto-list return-type address)
(let* ((call-name-serialized-proto-list
(loop for (call-name . proto)
in call-name-proto-list
for ser-proto
= (pu:serialize-proto-to-base64-string proto)
collect
(cons call-name ser-proto)))
(call-result
(or (drakma:http-request
address
:parameters call-name-serialized-proto-list)
"")))
(pu:deserialize-proto-from-base64-string return-type
call-result)))
Final Remarks
In this blog post we implemented several helper macros and a function for working with protocol-buffer objects in an HTTP environment. I believe the macros in protobuf-utilities are the missing link that will make cl-protobufs a welcome addition to Common Lisp HTTP servers.
Pull requests are always welcome

I would like to thank @rongut, @cgay, and @benkuehnert for their edits and comments.

















