21 comments

[ 1.9 ms ] story [ 41.1 ms ] thread
Sorry, why wouldn’t I write in the native language?
I'm primarily a Go developer and love the language and will defend it for most use-cases, but to be honest BPF seems like Rust's place to shine.
I think the real benefit here is being able to share structures, etc. with userspace and keep them in sync.

If this was compiling the Golang to BPF then yeah, that would feel ridiculous, but given that it's transpiling instead then, assuming that it's generating correct and reasonable code, I think this is certainly fine enough. Especially if you're just writing a proof of concept or something pretty basic, there's no reason not to start here.

If you're doing something like trying to filter 40Gbps of network traffic in eBPF then you'd probably want to consider something more hand-tuned/low-level, but that might well be a premature optimization for all I know.

What is BPF?
In eBPF-land you're going to be calling C functions in the kernel, and using (generally) C data types like structs and null-terminated strings. You can't do loops (loops are unrolled by the compiler), you can't do variadic functions, and you definitely can't take advantage of all the cool Go stuff like goroutines, select, context, etc.

I'm not really sure why you'd want to use this. If you're writing eBPF, you already need to know how to read C kernel source.

> Why transpile, not generate BPF directly

> gc, the Go compiler, has no LLVM-based BPF backend. Adding one is a multi-year compiler project. rustc is built on LLVM and that's why Aya works. So gobee emits C and reuses clang's BPF backend, which gives us mature codegen, BTF, and CO-RE relocations for free.

I wonder if TinyGo (https://tinygo.org/) might be a better fit here:

> TinyGo brings the Go programming language to embedded systems and to the modern web by creating a new compiler based on LLVM.

I've not played with TinyGo much so would be interested to hear other peoples experiences.

Remember, a lot of the memory safety benefits from go and rust and eBPF don't apply to the kernel eBPF! Kernel eBPF enforces semantics that verify array and loop bounds, memory accesses, and correctness of programs via the verifier. I think for most usecases, it is still best to write eBPF in C!
The readme is an immediate giveaway of sloppiness
try adding bindings to it from javascript and using it to render jsx in the terminal

https://yeet.cx

We write quite a bit of BPF at work for the Parca-Agent project[1]. And we find that even C is sometimes too high level of a language paired with modern optimization techniques as the code you write often doesn’t come out that way the other and and we have to resort to weird hacks or write assembly directly to appease the verifier.

Apart from that, the usual qualms one might have about C are not really relevant in eBPF land, so I’ve actually found it the nicest experience writing C I’ve ever had, the verifier is just the price we have to pay.

[1] https://github.com/parca-dev/parca-agent

Lately I’ve been using Go for a personal project and I am so so happy about it. So so happy.
I hate writing in Golang, I really do, but I cannot deny that it does what it set out to do extremely well.

The tooling is phenomenal and fast. It won't let me accidentally not use a variable, meaning that it won't let me foo, err := something() and not check err. It makes a lot of stuff explicit (e.g. there's no `array.add(item)`, just `array = append(array, newitem)` which makes it more obvious that I might be creating a lot more arrays than just the one I'm trying to work with, but it lets me do `make([]string, 5000)` to pre-allocate the length I want if I know what I need.

Every variable type has a default 'empty' value that is a valid value; an int with no value assigned is 0, a string with no value assigned is "", so you never get corrupted or random data when one of your branches doesn't set the value.

It has a lot of nice thread-safety stuff, since goroutines are such a thing. There's built-in functionality to say "Spawn all these goroutines and then wait until they're done", but there's also functionality to say "Here's a function, it should be called at most once across the lifetime of the program" so that you don't have to manually synchronize "did I do this initialization yet? Is it done yet? Get a lock and then check everything and then set everything."

And it's fast. It's really, really fast. It's so fast that I was testing a GOCACHEPROG program to cache intermediate compilation results instead of recompiling them and in at least some cases it was faster to recompile than to use the cache. The cache was a cloud storage bucket in another country, mind you, but with Rust or C++ that would still be a huge win. With Golang I had to work really, really hard to get cloud storage of intermediate artifacts to be faster than just recompiling on my laptop.

So yeah, I hate Golang and I hate writing Golang but... yeah, it's pretty good.

Noob question: why did they not choose to use WebAssembly in the kernel instead?
eBPF has a lot more checks in the verifier, which you could read about here if you're interested in learning more: https://docs.kernel.org/bpf/verifier.html

Since you don't want to handle any kind of crash, out of bounds exception, etc., the eBPF verifier does a ton of impressively paranoid stuff. It ensures that the program doesn't loop (or if it loops that the loop is provably bounded and cannot be infinite), it guarantees that you don't read from a register that might not have been written to, etc.

Basically, it needs to be able to mathematically prove without a doubt that the program behaves as it's supposed to or the verifier refuses to load it at all. WASM doesn't do that, since WASM is a general-purpose 'machine' and WASM programs could theoretically just run forever in entirely reasonable cases.

Fun fact: naming identifiers with leading underscores in C conflicts with reserved use and should always be avoided. I noticed Gobee declares double-underscores liberally.

Per 6.4.3 (Identifiers) of C23 (ISO/IEC 9899:202y N3886):

  — All identifiers that begin with a double underscore (__) or begin with an underscore (_) followed by an uppercase letter are reserved for any use, except those identifiers which are lexically identical to keywords.
  — All identifiers that begin with an underscore are reserved for use as identifiers with file scope in both the ordinary and tag name spaces.
https://open-std.org/JTC1/SC22/WG14/www/docs/n3886.pdf

And per 7.1.3 (Reserved identifiers) of C11 (ISO/IEC 9899:201x N1570):

  — All identifiers that begin with an underscore and either an uppercase letter or another underscore are always reserved for any use.
https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf