On this page:
5.1 What Cap  TP gives you
5.2 Cap  TP Limitations
5.3 Cap  TP usage example
5.4 Cap  TP API
make-machinetp-thread
setup-captp-vat
7.8

5 Distributed programming with CapTP

 (require goblins/captp) package: goblins

CAUTION: CapTP support is still in early days. Things will change, and some things (such as sturdyrefs and "three-machine introductions") are missing. Please do not rely yet on this protocol being stable. Also be aware that the interface provided in this release is known to be suboptimally "manual"; in future releases the system will help users set up CapTP connections more easily.

CapTP is a system for enabling distributed programming in a mutually suspicious network. It originated in the E programming language but has since appeared in Cap’N Proto, Waterken, Agoric’s SwingSet, and now Goblins.

5.1 What CapTP gives you

All of these combine into an efficient and powerful protocol that, most powerfully of all, the author of Goblins programs mostly need not think too much about. This circumvents the usual years and years of careful message structure coordination and standardization for many kinds of distributed programs, which can simply be written as normal Goblins programs rather than bespoke protocols.

5.2 CapTP Limitations

5.3 CapTP usage example

As said in CapTP Limitations, we don’t currently have an "easy" way to connect together CapTP connections. For the moment you have to manually wire them together. In the future this will be fixed.

In the meanwhile, here’s a simple version. What we are going to do is write two programs which talk over local "unix domain sockets". However you could just as easily use an OpenSSL connection or Tor Onion Services or whatever you choose.

Here’s our server, save as captp-uds-server.rkt:

#lang racket
 
(require goblins
         goblins/captp
         racket/unix-socket)
 
;; The ^car-factory example from the promise pipelining part
;; of the Goblins tutorial
(define (^car-factory bcom company-name)
  (define ((^car bcom model color))
    (format "*Vroom vroom!*  You drive your ~a ~a ~a!"
            color company-name model))
  (define (make-car model color)
    (spawn ^car model color))
  make-car)
 
;; Handle a new incoming connection on the unix domain socket
(define (handle-uds-listen uds-listener bootstrap-actor)
  ;; Accept the connection, getting a new input and output port
  (define-values (ip op)
    (unix-socket-accept uds-listener))
  ;; Set up a machinetp thread using this input and output port pair
  (define _remote-bootstrap-vow
    (make-machinetp-thread ip op
                           ;; the bootstrap actor is whatever we
                           ;; passed in (in this case, the car factory)
                           bootstrap-actor))
  (displayln "*** New connection!"))
 
(define (run-server uds-path)
  ;; A vat to spawn some objects in
  (define car-vat
    (make-vat))
 
  (define fork-motors
    (car-vat 'spawn ^car-factory "Fork"))
 
  ;; Listen to the unix domain socket at uds-path
  (define uds-listener
    (unix-socket-listen uds-path))
 
  (displayln "*** Server up and running!")
 
  (dynamic-wind
    void
    (lambda ()
      (let lp ()
        (sync (handle-evt uds-listener
                          (lambda _
                            (handle-uds-listen uds-listener fork-motors))))
        (lp)))
    (lambda ()
      (delete-file uds-path))))
 
(module+ main
  (command-line
   #:args (uds-path)
   (run-server uds-path)))

Now save the following client code as captp-uds-client.rkt:

#lang racket
 
(require goblins
         goblins/captp
         goblins/actor-lib/bootstrap
         racket/unix-socket)
 
(define (run-client uds-path signal-when-done)
  ;; We'll use this vat to run some code and set up our bootstrap actor
  (define a-vat
    (make-vat))
 
  ;; Here's where we'll connect to the unix domain path
  (define-values (ip op)
    (unix-socket-connect uds-path))
 
  (define remote-bootstrap-vow
    (make-machinetp-thread ip op))
  ;; Well we're being incredibly lazy and the remote bootstrap actor
  ;; *is* the car factory, so let's just alias that
  (define car-factory-vow remote-bootstrap-vow)
 
  (displayln "*** Connected to server")
 
  (define (get-and-drive-car)
    ;; Get a promise to get a new car
    (define car-vow
      (<- car-factory-vow "Explorist" "blue"))
    ;; Pipeline a promise with the noise from driving it
    ;; (The car takes no arguments on invocation to drive)
    (define drive-noise-vow
      (<- car-vow))
    ;; Now listen to the resolution of the pipelined driving noise.
    (on drive-noise-vow
        (lambda (drive-noise)
          ;; Yay, promise pipelining worked!
          (displayln (format "We hear: ~a" drive-noise))
          ;; Okay now signal that we can shut this down
          (semaphore-post signal-when-done))
        #:catch
        (lambda (err)
          (displayln (format "UHOH!!! Something went wrong: ~a" err)))))
  ;; Now run the above thunk on a-vat
  (a-vat 'run get-and-drive-car))
 
(module+ main
  (define signal-when-done
    (make-semaphore))
  (command-line
   #:args (uds-path)
   (run-client uds-path signal-when-done))
  ;; Keep this open until we've finished our communication, since the
  ;; rest of the program runs in a thread.
  (semaphore-wait signal-when-done))

In general when connecting to a captp connection without prior introductions in the network, you need some way of getting capabilities. In many cases this will be a generalized "sturdyref" provider (either use a random nonce or invoke a certificate chain to look up the first capability you’ve been authorized access to... see spawn-nonce-registry-locator-pair as one example of how to do this) but we’re hand-waving past that for the sake of minimalism here. Instead we’re just slotting the car factory itself as the capability the user is connecting to.

Okay, let’s try it out. Open up two terminals, and in the first one run:

racket captp-uds-server.rkt /tmp/captp-test.sock

And in the second run:

racket captp-uds-client.rkt /tmp/captp-test.sock

You should see:

*** Connected to server

We hear: *Vroom vroom!*  You drive your blue Fork Explorist!

If that worked, your connection succeeded, and so did promise pipelining... horray!

5.4 CapTP API

procedure

(make-machinetp-thread network-in-port    
  network-out-port    
  [bootstrap-obj]    
  #:captp-vat procedure?)  local-promise?
  network-in-port : input-port?
  network-out-port : output-port?
  bootstrap-obj : any/c = #f
  procedure? : (make-vat)
Setup a combination MachineTP thread and CapTP process which communicates over network-in-port and network-out-port. The remote side will be provided bootstrap-obj as the resolution of its bootstrap promise. If #:captp-vat is specified, then this should be a vat connector such as one returned from "make-vat", but if one is not provided a vat for this captp process will be set up specially for it.

Returns a promise for the bootstrap object on the other end.

procedure

(setup-captp-vat captp-vat    
  send-to-remote    
  [bootstrap-obj])  
local-object? live-refr?
  captp-vat : procedure?
  send-to-remote : (-> record? any/c)
  bootstrap-obj : any/c = #f
An even more manual way to set up a captp connection than make-machinetp-thread.

captp-vat is a vat connector (such as one returned from make-vat), which is where the various actors which manage this captp connection’s state will live. send-to-remote is a procedure which takes a Syrup record which it will send across some connection. The remote side will be provided bootstrap-obj as the resolution of its bootstrap promise.

Returns two values to its continuation, the first being a reference to an actor which receives messages from the machinetp connection, and the second being a promise to the remote object’s promise resolver.

With this first returned value and the send-to-remote procedure, we effectively have a way to receive and send messages over captp and we could map this on top of a network layer or something else. This is exactly what make-machinetp-thread does with ports.