Hello World.

This is the classic, prepared in the style of microservices and finished with drizzle of farm-raised unit testing.

If you want to just get straight to the stuff you gotta type use the nav bar on the right to jump to Source Code Files or even the hello world code proper.


One of the purposes of the ur-version of hello world was to show readers that a textually small program could be self-contained and do something useful.

	main( ) {
 		extrn a, b, c;
 		putchar(a); putchar(b); putchar(c); putchar(’!*n’);
	}
	a ’hell’;
	b ’o, w’;
	c ’orld’;

A Tutorial Introduction to the Programming Language B, B. Kernighan, 1973.

We are going to walk through all the files in the helloworld example. We will pay special attention to the make targets as these are re-usable and in many cases explain “how to do things” with parigot.

Boilerplate files

We’ll start with the boilerplate files that any project will need for doing development. We will describe the purpose of each of these files, even if their content can be largely ignored for now.

Makefile

The project’s Makefile is really just a repository for useful commands, not something that does builds by looking at files and computing new files based on recipies. Because golang has a much better build ability than most other languages, typically you can just do make anytime and let the Makefile try to engage the go build process. Go will do nothing if no compilation is needed.

Make commands (“targets”)

all

all builds the two wasm files that make up the program, hello.p.wasm and greeting.p.wasm. This is the default command, so it is run if you type just make. The two wasm files are built into the build subdirectory. The build is sufficiently fast that doing make clean; make is also ok.

The all command invokes the compiler to build the wasm (guest) code. This code sets the GOOS and GOARCH environment variables to convince the google go compiler to emit WASM code. As was stated before, because the go compiler does dependency analysis of the go source, no attempt is made to elaborate dependencies in the Makefile.

This all target’s command in the Makefile would be the part that would be replaced by a call to other go compilers (gccgo, tinygo) or compilers for other compiled programming lanugages like C#, Java, or Rust. The output files are suffixed with “.p.wasm” to indicate that they are intended to be dynamically linked against parigot; without parigot, these programs will not execute in any Host.

test

The test make target builds an runs the example test. test uses the standard go testing framework. This test command is useful if you want to run unit tests that are completely guest-side code. Because WASM isn’t directly executable, go test will not work, you have build a “program” that is the test and then run that program via a Host. This test uses wazero CLI as the Host, since it is already present in our dev container, but any host should work.

"generate" is the most key-to-parigot make target, as it generates stubs. These stubs implement the golang-specific type safe code that makes the parigot programming model work for golang developers. You need to run this anytime you change the `greeting.proto` protobuf schema definition contained in the `proto` directory. The default make target all runs `generate` just to be safe, even if it is not needed.

generate

The generate target is a key part of the Makefile and the build process more generally. This make target runs three key commands. First, it uses buf to run a lint pass over your .proto files. Then it uses buf to generate the typical golang types that derive from your protobuf schema (contained in the proto directory). Third, it uses protoc-gen-parigot which is a plugin for the protoc protobuf compiler that generates parigot’s stubs.

In the case of helloworld you’ll notice that the file proto/greeting/v1/greeting.proto which is the protobuf schema for the greeting service in our hello world program. Running buf generate results in the files g/greeting/v1/greetingserver.p.go, g/greeting/v1/greetingserviceddecl.p.go, and g/greeting/v1/greetingserver.p.go.

The lint settings that are enforced with buf lint are set in proto/buf.yaml and they are largely what is recommended by the buf team (who know their stuff). The places where it deviates are largely because of the repetitiveness of names that result. For example, FileServiceServer and KernelErrError seem like a bridge too far. Changing these settings in the buf.yaml file is not recommended if you are working with parigot.

The buf generate step generates the standard, probuf golang types based on protoc-gen-go.

clean

The clean make target removes all the generated files in the g/ directory and the compiled binaries (.p.wasm files) in build. The ‘g’ stands for generated and only automated tool’s output belongs there. make clean does not remove the tools installed by make tools in the next section.

buf.gen.yaml, buf.work.yaml

Both of buf.gen.yaml and buf.work.yaml are configuration files that most folks do not need to change. The buf.gen.yaml file tells buf what code generators to use. These are currently the golang (standard) one and the parigot one. The buf.work.yml file tells buf where to look for your protobuf schema files, but is recommended that all your protobuf files be in a top level directory called proto.

Source code files

That g directory

We covered the g directory previously in our discussion of the generate target.

The g directory’s content is all machine generated, and thus it always ok to delete. You can always create the content again with make generate. The g directory, sadly, is probably something you should check into your repo, despite the fact that the content is generated and that practice is generally discouraged. The reason for this is that someone might be using your project as a go module (as you are doing with parigot) and needs to be able to compile your code as part of a their build when they don’t have your module’s source installed. If g is not checked into your repository, the other party’s build will fail because the generated code in g is needed.

go.mod and go.sum

These are the standard files used by the go compiler to do dependency management and versioning. The contents of go.mod is probably enough for most projects when they are starting out.

helloworld.toml

This file is the delpoyment descriptor for the hello world program. Its contents tell the runner program the information about the microservices, tests, and programs that need to be deployed to make the entire application run. The contents as of the time of writing are:

## Note that because this file is expected to be consumed in a dev container
## so the files have absolute paths that are correct for the container.

## dev configuration in general
[config.dev]
## Load parigot code from shared object
ParigotLibPath="/workspaces/parigot/build/syscall.so"
ParigotLibSymbol="ParigotInitialize"
Timezone = "US/Eastern"
[config.dev.Timeout]
Startup = 100 # millis
Complete = 20 # millis


# greeting service
[config.dev.microservice.greet]
WasmPath="/workspaces/parigot/example/helloworld/build/greeting.p.wasm"
Arg=[]
Env=[]

# helloworld, it has no services that it implements, it just consumes greet and
# runs to the end of main.
[config.dev.microservice.helloworld]
WasmPath="/workspaces/parigot/example/helloworld/build/hello.p.wasm"
Arg=[]
Env=[]
# this is the crucial line for parigot. "this is just a client and should run to completion".
Main=true

main.go

This is the meat and cheese of the helloworld code. This is the main program that “drives” the hello world application. Unlike most applications of parigot, which just “exist”, this program runs to completion.

imports

	package main

import (
	"context"
	"log"
	"runtime/debug"

	"github.com/iansmith/parigot-example/helloworld/g/greeting/v1"
	syscallguest "github.com/iansmith/parigot/api/guest/syscall"
	apishared "github.com/iansmith/parigot/api/shared"
	"github.com/iansmith/parigot/api/shared/id"
	pcontext "github.com/iansmith/parigot/context"
	"github.com/iansmith/parigot/g/file/v1"
	"github.com/iansmith/parigot/g/syscall/v1"
	lib "github.com/iansmith/parigot/lib/go"
	"github.com/iansmith/parigot/lib/go/exit"
)

The imports are quite straightforward and should be supplied automatically by any sensible IDE (especially if your IDE uses gopls). Of mild interest are the aliases syscallguest and lib.

  • syscallguest: This alias is necessary to not conflict with the import of github.com/iansmith/parigot/g/syscall/v1 which is imported as syscall. syscallguest has the implementation to the system calls (of parigot) for guest code in golang. The syscall one is the definitions of the system calls generated from the proto spec.

  • lib: The golang guest side library uses the import name “lib” despite being .../lib/go. The alias is actually superfluous but was inserted by VSCode.

hello world code proper

In the initialization part of helloworld there is one key thing to notice:

futures in launch

The call to LaunchClient() in theory would block the code from proceeding until all of it is dependencies listed above MustInitClient are up and running. Since in practice we cannot block, the returned value is a future that we attach success and failure functions to.

The mainloop of hello world is the critical portion, centered around ReadOneAndCallClient. This is primarily for exposition, the call MustRunClient is more commonly used in such a situation.

afterLaunch

afterLaunch() is called from the success branch of the launch future. It does two things. One, it “locates” some service that implements the greeting service protocol. It then calls the method FetchGreeting() on the located service, and associates success and failure functions to the future resulting from call to FetchGreeting().

“Locate” is a key operation in parigot. Locate is the one that one turns the abstraction of a interface name, like “file.v1.File” into an object which obeys the protocol defined to be implemented by that interface. Typically, the file file.proto is going to have the functions and data associated with all the operations of a “file.v1.File”. The true implementation of the interface “file.v1.File” may be in a different program, a different container, or a different machine. This does not matter to the caller, as it is only concerned that the object that it has a reference to and names a “file.v1.File” is something that can understands the methods defined in the file.proto specification. has the functions

Per our exposition above, this version of the library uses continuations and you seem them in action with the method call of FetchGreeting which is defined by greeting’s protobuf specification. This continuation, in short, means that we do not yet know the outcome of the call, so you need to handle both the Success and Failure cases. In this example, it just prints out a message on the terminal.

Greeting service implementation

“main” of Greeting

Every service–and every program like helloworld–has a main() function to initialize data structures and the like at startup.

MustInitClient

The service implementation of greet uses the generated function Init() to initialize and launch itself. As we saw with the main() function of helloworld, we are returned a future that represents the success or failure of launching the program (and waiting on its dependencies). An additional parameter returned from Init() is a set of method bindings called bindings. This set of bindings is only useful if you want to manipulate the set of methods that this service responds to. It is not something that many programs will ever need. This service method map will passed to the generated Run() method.

fetchGreeting and FetchGreeting are now defined by the implementation of the greeting service. Because the greeting service has a known method, FetchGreeting, no futher initialization work is needed and the method can be implemented as FetchGreeting (as seen in the greeting.proto file). The of the “split” versions of “FetchGreeting” is so that the true call implementation, fetchGreeting() can be called directly from a test. The return value of FetchGreeting is naturally a future and these can be hard to test without running the “main loop” of parigot. See greeting_test for how a method like fetchGreeting is tested without using futures.

The Big Trick

You may have noticed a contradicton above: Both the code for the hello world main program and the code for the greeting service have a call to a “Run function” and use futures. The run loop in addition to futures (continuations) means that both of these are running single-threaded…. but, wait we cannot have two programs both running singly threaded can we?!?

parigot makes each program that has a “main” function a single-threaded guest program, in WASM terminology. In parigot terminology, each of these two singly threaded programs is a process. They behave much like processes in Unix/Linux for several reasons:

  • Each process runs until it finishes or exits due to an error (perhaps due to panic).
  • Two or more processes can and do run at the same time.
  • Programs are memory and code isolated from one another.
  • These processes use an InterProcess Communication (IPC) mechanism to exchange information. In parigot, these are the calls to services.

The careful reader will have noticed that this four point definition above could be generalized slightly from “processes” to “processes that run on different machines separated by a network” since all the same properties apply. Parigot does indeed generalize this and decides how to deploy the application based on the deployment descriptor. With the code remaining unchanged but with a different deployment descriptor, you can run your application as these “guest” processes inside a single WASM Host (and on a single host!), or it will create multiple WASM host programs on multiple machines, each with a single service. In this latter case, all the calls between services are network calls. That’s a microservice architecture!

greeting_test.go

The greeting test is quite simple: send one request (not using any parigot machinery) to the private, implementation function fetchGreeting.

We test that we got back what we expected in terms of the response object and the error code. Then we repeat the test, but with an out of bounds language number (defined as zero in the enum definition).

This test can run in pure WASM as a guest program, as we mentioned before. From a quality and ease of testing standpoint, the fact that this test does not require any booted-up service to be running makes it a true unit test.

Decorative files

Files that are not strictly necessary.

README.md

Explanation of parigot’s hello world program, in very terse form, for those that are browsing on github. Almost by definition, these fools (!) are not reading this document!

helloworld.code-workspace

The file helloworld.code-workspace contains the project level settings for development of the project in VSCode. The file is a jsonc document.