██████╗ ███████╗███╗ ██╗███████╗████████╗
██╔══██╗██╔════╝████╗ ██║██╔════╝╚══██╔══╝
██████╔╝█████╗ ██╔██╗ ██║█████╗ ██║
██╔══██╗██╔══╝ ██║╚██╗██║██╔══╝ ██║
██████╔╝██║ ██║ ╚████║███████╗ ██║
╚═════╝ ╚═╝ ╚═╝ ╚═══╝╚══════╝ ╚═╝
Just an HTTP server, except brainfuck runs the entire thing :3
Every "brainfuck web server" you'll find out there cheats the same way: some real language handles the socket, brainfuck just fills in a response body. Fair enough, brainfuck genuinely can't do networking - its only I/O is a byte in, a byte out.
bfnet closes that gap with a handful of extra instructions, kept as narrow
and single-purpose as possible rather than exposing a generic syscall
gateway. The accept loop, connection handling, routing, even outbound
connections - all of it lives inside the .bf file. bfnet itself doesn't
know what HTTP is; it's an instruction dispatcher and nothing else.
client ──TCP──▶ bfnet
│
▼
executes `~` or `&` ───▶ waits for / forks on a connection
│
▼
tape runs, `,` `.` `?` `@` all operate on that connection
│
▼
loops back around for the next one (or the thread just ends)
This is not standard brainfuck. A vanilla interpreter treats the extra instructions as comment characters and none of this works. If you want something that runs on any bf interpreter out there, this isn't it - that would mean putting the networking outside brainfuck entirely, which defeats the point of this repo.
+ - |
increment / decrement the current cell |
< > |
move the pointer left / right |
[ ] |
standard brainfuck loop |
, |
read one byte from the current connection |
. |
write one byte to the current connection |
~ |
close the current connection if any, block for the next one, wire it up as the , / . target |
& |
accept like ~, but fork: a new thread continues after & with the connection and cell = 1, the original thread also continues after & with cell = 0 and no connection, and loops back for the next one |
? |
write the peer's IPv4 + port into the current cell and the five after it (4 bytes IP, 2 bytes port), no pointer movement |
@ |
connect out to the NUL-terminated "host:port" string starting at the current cell; on success it becomes the new , / . target and the current cell gets set to 1, otherwise 0 |
8 standard, 4 new. , and . don't change meaning, they just stop pointing
at stdin/stdout and start pointing at whatever connection the current
thread owns.
cargo build --release
Binary lands at target/release/bfnet, a few hundred KB, no runtime deps.
Prebuilt binaries for Linux, macOS and Windows are attached to every release - grab one directly if you don't want to build it yourself.
bfnet program.bf [port]
Port defaults to 8080.
bfnet examples/router.bf 8080
curl localhost:8080/about
| File | Uses | Does |
|---|---|---|
examples/hello-json.bf |
~ |
fixed JSON body, same for every request |
examples/hello-text.bf |
~ |
fixed plain text body, same for every request |
examples/concurrent.bf |
& |
same fixed response as hello-json, but connections actually overlap instead of queueing - try two requests at once |
examples/whoami.bf |
& ? |
prints the caller's own IP and port back at them, in readable decimal |
examples/notify.bf |
& @ |
answers the caller, then fires a fixed request at a second host as a side effect |
examples/router.bf |
& |
real path-based routing (/, /about, /hello, 404 otherwise), byte-compared, no shortcuts |
A minimal server, in full:
+[~>[-]<response bytes go here>.<]
+sets cell 0 to1and nothing ever touches it again, so[...]never exits~blocks for a connection>[-]moves to a scratch cell and zeroes it out - it's dirty from whatever the last response left behind- then come the actual response bytes
<moves back to cell 0 so the loop condition still holds next round
Swap ~ for & and you get concurrency, at the cost of needing to check
which thread you are - see examples/concurrent.bf for the shape of that.
Writing responses byte-by-byte by hand works but gets old fast.
tools/text2bf.py takes raw bytes on stdin and prints a ~-based program
in exactly that shape:
cat response.txt | tools/text2bf.py > examples/whatever.bf
For anything with actual logic - routing, decimal formatting, and so on -
tools/bfbuilder.py is a small Python code builder that tracks pointer
position for you instead of hand-counting < and >. tools/route_gen.py
and tools/build_examples.py are built on top of it and are a reasonable
place to start if you want to see it used for something non-trivial. None
of this is needed at runtime - the binary has zero Python involvement.
| Fixed response for any request | ✅ |
| Multiple sequential connections, one process | ✅ |
Concurrent connections (&) |
✅ |
Peer IP/port visible to the program (?) |
✅ |
Outbound connections (@) |
✅ |
| Path-based routing, parsed byte by byte in bf | ✅ |
| Content-Length computed at runtime instead of build time | 📅 |
| True relay/proxying (two live connections at once) | 📅 |
| UDP | 📅 |
- one connection per thread - concurrency needs
&, plain~still serves one at a time - a malformed program (unmatched brackets, pointer walking off either end of the tape) takes down whichever thread hit it - the main process and other in-flight connections are unaffected
@only gives you one connection slot, same as~/&. Once you connect out, the original inbound connection is gone - there's no way to read from one connection and write to another at the same time, so true proxying isn't possible yet.notify.bfworks around this by answering the real client before ever touching@- Content-Length is either baked in at build time (when the response is
fixed) or left out entirely with
Connection: closerelied on instead (when it isn't, like inwhoami.bf)
See CHANGELOG.md for what changed between releases.