Guile Blocks: I Have Concepts of a Plan

I recently released v0.1.0 of Guile Blocks, an Org Mode inspired implementation of source blocks in Guile Scheme. This post demonstrates some of the functionality I've been working on since then.

PlantUML

PlantUML diagrams are now supported.

@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response

Alice -> Bob: Another authentication Request
Alice <-- Bob: Another authentication Response
@enduml

PlantUML was tricky to support. This is because of how PlantUML handles writing and naming the output diagram. By priority, PlantUML names the output diagram in one of two ways:

  1. Is there a filename in the UML code? e.g. @startuml <filename> ... @enduml
  2. What is the filename passed on the command line? e.g. $ plantuml <filename>

Option 1 is interesting, but in order to print the block, I need the PlantUML evaluator to return the filename. PlantUML is stingy and does not print the filename to stdout. This means the only way to get the output filename is by parsing the UML diagram in Guile.

This wouldn't be too bad, but unfortunately these diagrams are not restricted to @startuml tags. They may also be @startjson, @startsalt, and more. A regex could handle this, but that solution feels... icky. I want to minimize the linkage between Guile Blocks and the supported languages when possible.

Option 2 also isn't great. Behind the scenes Guile Blocks creates a temporary file (of the form /tmp/fileXXXXXX) and passes that to PlantUML. This means that any diagram that does not contain a filename in its code will have a random name.

Worse, PlantUML also does not write the output diagram to the current working directory. For whatever reason it writes to the same directory as the input file. Writing diagrams to /tmp isn't very useful.

So, if both option 1 and option 2 aren't viable, I'm left with no other choice but to create a 3rd option. To do this, I use a comparatively niche PlantUML feature, pipe output. When -pipe is passed, PlantUML writes the output diagram to stdout instead of a file. From there I can capture the diagram as a bytevector and write it to a file in Scheme.

(define* (run-plantuml code name extension flags)
  (let ((format-flag (string-append "-t" extension))
        (output-dir (dirname name))
        (filename (string-append name "." extension)))
    ;; Guile will error if the output directory does not exist.
    (mkdir-p output-dir)
    (call-with-output-file filename
      (lambda (port)
        (put-bytevector
         port
         ;; Output is a raw image, not a string. Avoid corruption
         ;; caused by conversion to string.
         ((bv-in->string-in eval-from-stdin-bytevector)
          code
          %plantuml-command
          `(,@flags ,format-flag "-pipe")))))
    filename))

This did require reworking my evaluator functions to be a bit more generic.

Bytevector Evaluators

Previously, Guile Blocks was designed around string-based evaluators, or evaluators that take and receive strings, when invoking external processes. These work well in most cases. However, the process of encoding an image into a string and writing said string to a file corrupts the image (at least for PNGs). To resolve this, I rewrote the evaluators to operate on bytevectors and added a layer of wrapper functions to convert those bytevectors to and from strings.

I quite like how the bytevector translation functions look.

(define* (bv-in->string-in bv-func #:key encoding)
  (lambda (string . args)
    (apply bv-func
           (string->bytevector string
                               (or encoding
                                   (fluid-ref %default-port-encoding)))
           args)))

(define* (bv-out->string-out bv-func #:key encoding)
  (lambda args
    (let ((result (apply bv-func args)))
      ;; Conditional as bv-func may return #<eof> which is not a
      ;; bytevector.
      (if (bytevector? result)
          (bytevector->string result
                              (or encoding
                                  (fluid-ref %default-port-encoding)))
          result))))

(define* (bv-in-out->string-in-out bv-func #:key encoding)
  (bv-in->string-in (bv-out->string-out bv-func #:encoding encoding)
                    #:encoding encoding))

I find it neat how generic these functions are. Any function that takes a bytevector as its first argument can be wrapped to take a string instead, independent of the rest of its signature. The same goes for any function that returns a bytevector.

This allows mo to trivially support both string and bytevector based evaluators. Unfortunately the naming scheme is still a bit clunky.

C

C code is also supported.

#include <stdio.h>

int main() {
  printf("Hello world!\n");
}
Results:
Hello world!

At present this support is fairly barebones. Snippets are compiled to a temporary file using GCC and executed.

The only interesting trick here is learning about GCC's -x flag. Like before, the C code is stored in a /tmp/fileXXXXXX file and passed to GCC on the command line. It turns out that when a file is missing an extension, GCC does not know how to compile it. I can override this by changing the invocation to $ gcc /tmp/fileXXXXXX -x c.

Supporting other GCC languages like C++ should be as simple as changing the extension.