Skip to content

Commit ccfafd9

Browse files
authored
nbio: add examples (#161)
* nbio: add examples * fix shadowing
1 parent 27ed871 commit ccfafd9

File tree

10 files changed

+506
-0
lines changed

10 files changed

+506
-0
lines changed

.github/workflows/check.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ jobs:
158158
odin check nanovg/example.odin -file $FLAGS
159159
odin check nanovg/fbo.odin -file $FLAGS
160160
161+
odin check nbio/context $FLAGS
162+
odin check nbio/game-loop $FLAGS
163+
odin check nbio/no-callbacks $FLAGS
164+
odin check nbio/sendfile $FLAGS
165+
odin check nbio/tcp-echo $FLAGS
166+
odin check nbio/udp-echo $FLAGS
167+
odin check nbio/workers $FLAGS
168+
161169
# TODO: fix orca examples after update.
162170
# odin check orca/breakout -target:orca_wasm32 $FLAGS
163171
# odin check orca/clock -target:orca_wasm32 $FLAGS

nbio/context/main.odin

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import "base:runtime"
4+
5+
import "core:nbio"
6+
import "core:time"
7+
8+
main :: proc() {
9+
nbio.acquire_thread_event_loop()
10+
defer nbio.release_thread_event_loop()
11+
12+
{
13+
context.user_index = 2
14+
15+
// By default, the context you receive in callbacks is the one that `nbio.run`/`tick` receives.
16+
nbio.timeout(time.Second, proc(op: ^nbio.Operation) {
17+
assert(context.user_index == 1)
18+
})
19+
20+
// If you want to pass through the current context, you could clone it, pass it through user data, and restore it.
21+
nbio.timeout_poly(time.Second, new_clone(context), proc(op: ^nbio.Operation, ctx: ^runtime.Context) {
22+
context = ctx^
23+
free(ctx)
24+
assert(context.user_index == 2)
25+
})
26+
}
27+
28+
context.user_index = 1
29+
nbio.run()
30+
}

nbio/game-loop/main.odin

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
Example game loop integrated with nbio, using Raylib here.
3+
4+
Showcases a simple async asset loader, textures are assigned a default texture at the start.
5+
When the texture is asked to be loaded it is done so using the event loop.
6+
*/
7+
package main
8+
9+
import "core:nbio"
10+
import "core:log"
11+
import "core:strings"
12+
13+
import rl "vendor:raylib"
14+
15+
Texture :: struct {
16+
path: cstring,
17+
width: int,
18+
height: int,
19+
data: rl.Texture2D,
20+
}
21+
22+
Texture_Name :: enum {
23+
Default,
24+
Raylib_Logo,
25+
}
26+
27+
g_textures := [Texture_Name]Texture{
28+
.Default = {#directory + "/missing.png", 32, 32, {}},
29+
.Raylib_Logo = {#directory + "/raylib_logo.png", 256, 256, {}},
30+
}
31+
32+
main :: proc() {
33+
context.logger = log.create_console_logger()
34+
35+
nbio.acquire_thread_event_loop()
36+
defer nbio.release_thread_event_loop()
37+
38+
rl.InitWindow(800, 600, "nbio")
39+
defer rl.CloseWindow()
40+
41+
load_texture(&g_textures[.Default])
42+
43+
rl.SetTargetFPS(60)
44+
45+
for !rl.WindowShouldClose() {
46+
// Note that it is probably not good to run your event loop bound by the frame rate.
47+
// There is this cool story about GTA5, which does this, and people had to develop a mod
48+
// that turned off vsync during loading screens (causing much more fps) which would speed
49+
// up the event loop and make the loading screen go by much faster.
50+
if err := nbio.tick(timeout=0); err != nil {
51+
log.errorf("nbio.tick: %v", err)
52+
}
53+
54+
w, h := f32(rl.GetScreenWidth()), f32(rl.GetScreenHeight())
55+
56+
{
57+
rl.BeginDrawing()
58+
defer rl.EndDrawing()
59+
60+
rl.ClearBackground(rl.RAYWHITE)
61+
62+
txt := &g_textures[.Raylib_Logo]
63+
draw_texture(txt^, {w/2 - f32(txt.width)/2, h/2 - f32(txt.height)/2, f32(txt.width), f32(txt.height)}, rl.WHITE)
64+
65+
if rl.GuiButton({w/2 - f32(txt.width)/2, h/2 + f32(txt.height)/2, f32(txt.width), 50}, "Load texture") {
66+
if rl.IsTextureReady(txt.data) {
67+
rl.UnloadTexture(txt.data)
68+
txt.data = {}
69+
} else {
70+
load_texture(txt)
71+
}
72+
}
73+
}
74+
}
75+
}
76+
77+
draw_texture :: proc(txt: Texture, dest: rl.Rectangle, tint: rl.Color) {
78+
txt := txt
79+
if !rl.IsTextureReady(txt.data) {
80+
txt.data = g_textures[.Default].data
81+
}
82+
83+
rl.DrawTexturePro(
84+
txt.data,
85+
{0, 0, f32(txt.data.width), f32(txt.data.height)},
86+
dest,
87+
{},
88+
0,
89+
rl.WHITE,
90+
)
91+
}
92+
93+
load_texture :: proc(txt: ^Texture) {
94+
assert(txt.width > 0)
95+
assert(txt.height > 0)
96+
assert(txt.path != nil)
97+
txt.data = g_textures[.Default].data
98+
99+
nbio.read_entire_file(string(txt.path), txt, on_read)
100+
101+
on_read :: proc(txt: rawptr, buf: []byte, err: nbio.Read_Entire_File_Error) {
102+
txt := (^Texture)(txt)
103+
if err.value != nil {
104+
log.errorf("load_texture(%q): %v: %v", txt.path, err.operation, err.value)
105+
return
106+
}
107+
108+
spath := string(txt.path)
109+
i := strings.last_index(spath, ".")
110+
assert(i > 0)
111+
ext := spath[i:]
112+
113+
image := rl.LoadImageFromMemory(cstring(raw_data(ext)), raw_data(buf), i32(len(buf)))
114+
txt.data = rl.LoadTextureFromImage(image)
115+
116+
rl.UnloadImage(image)
117+
delete(buf)
118+
119+
log.infof("load_texture: %q loaded", txt.path)
120+
}
121+
}

nbio/game-loop/missing.png

290 Bytes
Loading

nbio/game-loop/raylib_logo.png

3.59 KB
Loading

nbio/no-callbacks/main.odin

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
Example shows how you could poll completions instead of being pushed completions through callbacks.
3+
4+
Completions are added to a queue by a single simple callback, and handled whenever wanted.
5+
6+
It is a UDP echo server.
7+
*/
8+
package main
9+
10+
import "core:container/queue"
11+
import "core:fmt"
12+
import "core:nbio"
13+
14+
main :: proc() {
15+
nbio.acquire_thread_event_loop()
16+
defer nbio.release_thread_event_loop()
17+
18+
socket, err := nbio.create_udp_socket(.IP4)
19+
if err != nil {
20+
fmt.eprintfln("create_udp_socket: %v", err)
21+
return
22+
}
23+
if err := nbio.bind(socket, {nbio.IP4_Loopback, 1234}); err != nil {
24+
fmt.eprintfln("bind: %v", err)
25+
return
26+
}
27+
28+
buf: [1024]byte
29+
nbio.recv(socket, {buf[:]}, queue_callback)
30+
31+
for {
32+
fmt.println("tick")
33+
34+
for op in queue.pop_front_safe(&g_queue) {
35+
defer nbio.reattach(op) // Releases ownership of the operation.
36+
37+
fmt.printfln("%v completed", op.type)
38+
39+
#partial switch op.type {
40+
case .Recv:
41+
if op.recv.err != nil {
42+
fmt.eprintfln("recv: %v", op.recv.err)
43+
nbio.recv(socket, {buf[:]}, queue_callback)
44+
continue
45+
}
46+
47+
fmt.printfln("received %M", op.recv.received)
48+
nbio.send(socket, {buf[:op.recv.received]}, queue_callback, op.recv.source)
49+
50+
case .Send:
51+
if op.send.err != nil {
52+
fmt.eprintfln("send: %v", op.send.err)
53+
}
54+
fmt.printfln("sent %M", op.send.sent)
55+
nbio.recv(socket, {buf[:]}, queue_callback)
56+
57+
case:
58+
fmt.eprintfln("unimplemented: %v", op.type)
59+
}
60+
}
61+
62+
if err := nbio.tick(); err != nil {
63+
fmt.eprintfln("tick: %v", err)
64+
}
65+
}
66+
}
67+
68+
g_queue: queue.Queue(^nbio.Operation)
69+
70+
queue_callback :: proc(op: ^nbio.Operation) {
71+
nbio.detach(op) // Takes ownership of the operation.
72+
queue.push_back(&g_queue, op)
73+
}

nbio/sendfile/main.odin

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Example that shows the `sendfile` operation.
3+
4+
Dials a TCP server (localhost:1234) (use `nc -l 1234` to start a netcat server)
5+
and sends the contents of this file to it.
6+
7+
Try it with a big file and you will see progress updates as the file is sent.
8+
*/
9+
package main
10+
11+
import "core:fmt"
12+
import "core:nbio"
13+
import "core:os"
14+
import "core:terminal/ansi"
15+
16+
main :: proc() {
17+
if len(os.args) <= 1 {
18+
fmt.eprintfln("usage: %s <path>", os.args[0])
19+
return
20+
}
21+
22+
err := nbio.acquire_thread_event_loop()
23+
assert(err == nil)
24+
defer nbio.release_thread_event_loop()
25+
26+
nbio.dial({nbio.IP4_Loopback, 1234}, on_dial)
27+
28+
err = nbio.run()
29+
assert(err == nil)
30+
31+
on_dial :: proc(op: ^nbio.Operation) {
32+
fmt.assertf(op.dial.err == nil, "dial: %v", op.dial.err)
33+
34+
file, err := nbio.open_sync(os.args[1])
35+
assert(err == nil)
36+
37+
// Call can also take an offset, a length, a timeout.
38+
// By default it sends the entire file, without a timeout.
39+
sendfile_op := nbio.sendfile(op.dial.socket, file, on_sent, progress_updates=true)
40+
41+
fmt.print(ansi.CSI + ansi.DECTCEM_HIDE)
42+
render_progress(sendfile_op, clear=false)
43+
}
44+
45+
on_sent :: proc(op: ^nbio.Operation) {
46+
fmt.assertf(op.sendfile.err == nil, "sendfile: %v", op.sendfile.err)
47+
48+
render_progress(op, clear=true)
49+
if op.sendfile.sent < op.sendfile.nbytes {
50+
// Progress update, not done.
51+
return
52+
}
53+
54+
fmt.print(ansi.CSI + ansi.DECTCEM_SHOW)
55+
nbio.close(op.sendfile.file)
56+
}
57+
}
58+
59+
render_progress :: proc(op: ^nbio.Operation, clear: bool) {
60+
if clear {
61+
fmt.print(ansi.CSI + "1" + ansi.CUU)
62+
}
63+
64+
fmt.print(ansi.CSI + "2" + ansi.EL + ansi.CSI + "0" + ansi.CHA)
65+
fmt.printfln("%M/%M", op.sendfile.sent, op.sendfile.nbytes)
66+
}

nbio/tcp-echo/main.odin

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
This example shows a simple TCP server that echos back anything it receives.
3+
4+
Better error handling and closing/freeing connections are left for the reader.
5+
6+
You can run this with the netcat utility as the client like so: `nc 127.0.0.1 1234`.
7+
*/
8+
package main
9+
10+
import "core:container/xar"
11+
import "core:fmt"
12+
import "core:nbio"
13+
14+
Server :: struct {
15+
socket: nbio.TCP_Socket,
16+
// Xar is used in favor of `[dynamic]Connection` so pointers are stable.
17+
connections: xar.Array(Connection, 4),
18+
}
19+
20+
Connection :: struct {
21+
server: ^Server,
22+
sock: nbio.TCP_Socket,
23+
buf: [50]byte,
24+
}
25+
26+
main :: proc() {
27+
err := nbio.acquire_thread_event_loop()
28+
fmt.assertf(err == nil, "Could not initialize nbio: %v", err)
29+
defer nbio.release_thread_event_loop()
30+
31+
server: Server
32+
33+
socket, listen_err := nbio.listen_tcp({nbio.IP4_Loopback, 1234})
34+
fmt.assertf(listen_err == nil, "Error listening on localhost:1234: %v", err)
35+
server.socket = socket
36+
37+
nbio.accept_poly(socket, &server, on_accept)
38+
39+
rerr := nbio.run()
40+
fmt.assertf(rerr == nil, "Server stopped with error: %v", rerr)
41+
}
42+
43+
on_accept :: proc(op: ^nbio.Operation, server: ^Server) {
44+
fmt.assertf(op.accept.err == nil, "Error accepting a connection: %v", op.accept.err)
45+
46+
// Register a new accept for the next client.
47+
nbio.accept_poly(server.socket, server, on_accept)
48+
49+
connection, alloc_err := xar.push_back_elem_and_get_ptr(&server.connections, Connection{
50+
server = server,
51+
sock = op.accept.client,
52+
})
53+
assert(alloc_err == nil)
54+
55+
nbio.recv_poly(op.accept.client, {connection.buf[:]}, connection, on_recv)
56+
}
57+
58+
on_recv :: proc(op: ^nbio.Operation, connection: ^Connection) {
59+
fmt.assertf(op.recv.err == nil, "Error receiving from client: %v", op.recv.err)
60+
61+
nbio.send_poly(connection.sock, {connection.buf[:op.recv.received]}, connection, on_sent)
62+
}
63+
64+
on_sent :: proc(op: ^nbio.Operation, connection: ^Connection) {
65+
fmt.assertf(op.send.err == nil, "Error sending to client: %v", op.send.err)
66+
67+
// Accept the next message, to then ultimately echo back again.
68+
nbio.recv_poly(connection.sock, {connection.buf[:]}, connection, on_recv)
69+
}

0 commit comments

Comments
 (0)