Skip to content

Commit cba2c89

Browse files
Examples: Port bpfsnake, the game logic in eBPF and the board in Python
1 parent 35d1766 commit cba2c89

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

‎examples/snake.py‎

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Snake, with the game in the kernel.
2+
#
3+
# A port of bpfsnake (github.com/amiremohamadi/bpfsnake), a bpftrace script.
4+
# The eBPF side keeps the whole game state in maps and runs the game logic:
5+
# steering, moving, collisions, eating and growing. Python only draws the board
6+
# the kernel left in the maps, because bpftrace's printf is an event sent to
7+
# userspace and has no counterpart inside an eBPF program.
8+
#
9+
# Steering comes from a kprobe on pty_write, as in the original: a terminal
10+
# echoes each typed byte back with a one-byte write, so an arrow key's final
11+
# byte ('A' up, 'B' down, 'D' left; anything else is right) lands in `key`.
12+
# Leave the terminal's echo on, it is what the probe sees.
13+
#
14+
# bpftrace's `interval:ms:120` would be a perf_event program, which pylibbpf
15+
# cannot attach yet. Instead Python drives the clock: every tick it calls
16+
# getppid(), and a kprobe on that syscall, filtered to this process, advances
17+
# the game one step.
18+
#
19+
# Run `sudo /path/to/python examples/snake.py` from a terminal at least 50
20+
# columns wide. Ctrl-C quits. x86_64 only (pt_regs registers, syscall name).
21+
22+
import os
23+
import sys
24+
import termios
25+
import time
26+
from ctypes import c_int8, c_int64, c_uint64
27+
28+
from vmlinux import struct_pt_regs
29+
30+
from pythonbpf import BPF, bpf, bpfglobal, map, section
31+
from pythonbpf.helper import pid, probe_read_kernel, random
32+
from pythonbpf.maps import HashMap
33+
34+
MAX_LENGTH = 10
35+
WIDTH = 50
36+
HEIGHT = 12
37+
38+
# Slots in the `state` map. The eBPF functions spell them as literals: module
39+
# constants are not visible inside them.
40+
FOODX, FOODY, GAME_OVER, PLAYER = 0, 1, 2, 3
41+
42+
43+
# Body segment i sits at row snakex[i], column snakey[i]; segment 0 is the head.
44+
# A missing key reads as 0, which is a wall, so the body ends at the first 0.
45+
@bpf
46+
@map
47+
def snakex() -> HashMap:
48+
return HashMap(key=c_uint64, value=c_uint64, max_entries=10)
49+
50+
51+
@bpf
52+
@map
53+
def snakey() -> HashMap:
54+
return HashMap(key=c_uint64, value=c_uint64, max_entries=10)
55+
56+
57+
@bpf
58+
@map
59+
def state() -> HashMap:
60+
return HashMap(key=c_uint64, value=c_uint64, max_entries=4)
61+
62+
63+
@bpf
64+
@bpfglobal
65+
def key() -> c_int64:
66+
return c_int64(0)
67+
68+
69+
# pty_write(struct tty_struct *tty, const u8 *buf, size_t c)
70+
@bpf
71+
@section("kprobe/pty_write")
72+
def steer(ctx: struct_pt_regs) -> c_int64:
73+
global key
74+
if ctx.dx == 1:
75+
typed = c_int8(0)
76+
probe_read_kernel(typed, ctx.si)
77+
key = typed
78+
return c_int64(0)
79+
80+
81+
@bpf
82+
@section("kprobe/__x64_sys_getppid")
83+
def tick(ctx: struct_pt_regs) -> c_int64:
84+
if pid() != state.lookup(3): # PLAYER
85+
return c_int64(0)
86+
if state.lookup(2) != 0: # GAME_OVER
87+
return c_int64(0)
88+
89+
# Move: every segment takes the place of the one ahead of it
90+
i = 0
91+
while i < 10 and snakex.lookup(i) != 0 and snakey.lookup(i) != 0:
92+
i += 1
93+
i -= 1
94+
while i > 0:
95+
snakex.update(i, snakex.lookup(i - 1))
96+
snakey.update(i, snakey.lookup(i - 1))
97+
i -= 1
98+
99+
headx = snakex.lookup(0)
100+
heady = snakey.lookup(0)
101+
if key == 65: # up
102+
snakex.update(0, headx - 1)
103+
elif key == 66: # down
104+
snakex.update(0, headx + 1)
105+
elif key == 68: # left
106+
snakey.update(0, heady - 1)
107+
else: # right
108+
snakey.update(0, heady + 1)
109+
110+
# Game over when the head hits a wall ...
111+
x = snakex.lookup(0)
112+
y = snakey.lookup(0)
113+
if x == 0 or x == 11 or y == 0 or y == 49:
114+
state.update(2, 1) # GAME_OVER
115+
116+
# ... or the snake's own body
117+
i = 1
118+
while i < 10 and snakex.lookup(i) != 0 and snakey.lookup(i) != 0:
119+
if x == snakex.lookup(i) and y == snakey.lookup(i):
120+
state.update(2, 1) # GAME_OVER
121+
i += 1
122+
123+
# Eat: new food somewhere, and the tail grows by one
124+
if x == state.lookup(0) and y == state.lookup(1): # FOODX, FOODY
125+
state.update(0, random() % 10 + 1)
126+
state.update(1, random() % 10 + 1)
127+
i = 2
128+
while i < 10 and snakex.lookup(i) != 0 and snakey.lookup(i) != 0:
129+
i += 1
130+
snakex.update(i, snakex.lookup(i - 1))
131+
snakey.update(i, snakey.lookup(i - 1))
132+
133+
return c_int64(0)
134+
135+
136+
@bpf
137+
@bpfglobal
138+
def LICENSE() -> str:
139+
return "GPL"
140+
141+
142+
def read(m, k):
143+
return m.lookup(k) or 0
144+
145+
146+
def render(b):
147+
body = set()
148+
for i in range(MAX_LENGTH):
149+
x, y = read(b["snakex"], i), read(b["snakey"], i)
150+
if x == 0 or y == 0:
151+
break
152+
body.add((x, y))
153+
food = (read(b["state"], FOODX), read(b["state"], FOODY))
154+
155+
rows = []
156+
for x in range(HEIGHT):
157+
row = []
158+
for y in range(WIDTH):
159+
if x == 0 or y == 0 or x == HEIGHT - 1 or y == WIDTH - 1:
160+
row.append("#")
161+
elif (x, y) == food:
162+
row.append("$")
163+
elif (x, y) in body:
164+
row.append("@")
165+
else:
166+
row.append(" ")
167+
rows.append("".join(row))
168+
# One write per frame: the steering probe only looks at one-byte writes
169+
sys.stdout.write("\033[H" + "\n".join(rows) + "\n")
170+
sys.stdout.flush()
171+
172+
173+
b = BPF()
174+
b.load()
175+
b.attach_all()
176+
177+
# BEGIN
178+
b["snakex"][0] = 1
179+
b["snakey"][0] = 1
180+
b["snakex"][1] = 1
181+
b["snakey"][1] = 2
182+
b["state"][FOODX] = 4
183+
b["state"][FOODY] = 4
184+
b["state"][PLAYER] = os.getpid()
185+
186+
sys.stdout.write("\033[H\033[2J\033[?25l") # clear screen, hide cursor
187+
try:
188+
while True:
189+
os.getppid() # interval:ms:120
190+
render(b)
191+
if read(b["state"], GAME_OVER):
192+
print("game over")
193+
break
194+
time.sleep(0.12)
195+
except KeyboardInterrupt:
196+
pass
197+
finally:
198+
sys.stdout.write("\033[?25h")
199+
sys.stdout.flush()
200+
# The keys were echoed but never read: drop them before the shell sees them
201+
if sys.stdin.isatty():
202+
termios.tcflush(sys.stdin, termios.TCIFLUSH)

0 commit comments

Comments
 (0)