├── examples
├── hexagonal
│ ├── README.md
│ ├── src
│ │ ├── infrastructure
│ │ │ ├── database
│ │ │ │ ├── database.v
│ │ │ │ └── db_pool.v
│ │ │ ├── repositories
│ │ │ │ ├── dummy_product_repository.v
│ │ │ │ ├── sqlite_user_repository.v
│ │ │ │ └── pg_user_repository.v
│ │ │ └── http
│ │ │ │ ├── middleware.v
│ │ │ │ └── controllers.v
│ │ ├── domain
│ │ │ ├── auth_service.v
│ │ │ ├── product_repository.v
│ │ │ └── user_repository.v
│ │ ├── application
│ │ │ ├── auth_usecase.v
│ │ │ ├── product_usecase.v
│ │ │ └── user_usecase.v
│ │ └── main.v
│ └── HEXAGONAL_ARCHITETURE.md
├── sse
│ ├── .gitignore
│ ├── front-end
│ │ ├── favicon.ico
│ │ └── index.html
│ ├── README.md
│ └── src
│ │ └── main.v
├── etag
│ ├── .gitignore
│ ├── simple
│ ├── front-end
│ │ ├── favicon.ico
│ │ └── index.html
│ ├── src
│ │ ├── main.v
│ │ ├── main_test.v
│ │ └── controllers.v
│ └── README.md
├── simple
│ ├── .gitignore
│ └── src
│ │ ├── main_test.v
│ │ ├── main.v
│ │ ├── server_end_to_end_test.v
│ │ └── controllers.v
├── simple2
│ ├── .gitignore
│ └── src
│ │ ├── main_test.v
│ │ ├── controllers.v
│ │ └── main.v
├── database
│ ├── .gitignore
│ ├── docker-compose.yml
│ └── src
│ │ ├── database.v
│ │ ├── main.v
│ │ └── controllers.v
├── simple3
│ ├── .gitignore
│ └── src
│ │ ├── main_test.v
│ │ ├── server_end_to_end_test.v
│ │ ├── controllers.v
│ │ └── main.v
└── io_uring_demo
│ ├── io_uring_demo
│ ├── src
│ └── main.v
│ └── probe.c
├── http_server
├── http1_1
│ ├── http1_1.v
│ ├── request
│ │ └── request.c.v
│ ├── response
│ │ └── response.c.v
│ └── request_parser
│ │ ├── request_parser_test.v
│ │ └── request_parser.v
├── io_multiplexing
│ └── io_multiplexing.v
├── http2
│ ├── hpack.v
│ ├── frame.v
│ └── types.v
├── tls
│ ├── tls.v
│ └── tls_cert
│ │ └── tls_cert.v
├── http_server_linux.c.v
├── server_test.v
├── epoll
│ └── epoll_linux.c.v
├── kqueue
│ └── kqueue_darwin.c.v
├── http_server_darwin.c.v
├── socket
│ ├── socket_windows.c.v
│ └── socket_tcp.c.v
├── README.md
├── iocp
│ └── iocp_windows.c.v
├── http_server.c.v
├── http_server_io_uring_linux.c.v
├── http_server_epoll_linux.c.v
├── io_uring
│ └── io_uring_linux.c.v
└── http_server_windows.c.v
├── logo.png
├── .editorconfig
├── .gitattributes
├── v.mod
├── .gitignore
├── .github
└── workflows
│ ├── pretty_fmt_checker.yml
│ ├── build_test_examples_on_darwin.yml
│ ├── build_test_examples_on_windows.yml
│ └── build_test_examples_on_linux.yml
├── LICENSE
├── CONTRIBUTING.md
└── README.md
/examples/hexagonal/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/http_server/http1_1/http1_1.v:
--------------------------------------------------------------------------------
1 | module http1_1
2 |
--------------------------------------------------------------------------------
/examples/sse/.gitignore:
--------------------------------------------------------------------------------
1 | sse
2 | sse.exe
3 | sse.exe~
--------------------------------------------------------------------------------
/examples/etag/.gitignore:
--------------------------------------------------------------------------------
1 | etag
2 | etag.exe
3 | etag.exe~
--------------------------------------------------------------------------------
/examples/simple/.gitignore:
--------------------------------------------------------------------------------
1 | simple
2 | simple.exe
3 | simple.exe~
--------------------------------------------------------------------------------
/examples/simple2/.gitignore:
--------------------------------------------------------------------------------
1 | simple2
2 | simple2.exe
3 | simple2.exe~
--------------------------------------------------------------------------------
/examples/database/.gitignore:
--------------------------------------------------------------------------------
1 | database
2 | database.exe
3 | database.exe~
--------------------------------------------------------------------------------
/examples/simple3/.gitignore:
--------------------------------------------------------------------------------
1 | simple3
2 | simple3.exe
3 | simple3.exe~
4 |
--------------------------------------------------------------------------------
/http_server/io_multiplexing/io_multiplexing.v:
--------------------------------------------------------------------------------
1 | module io_multiplexing
2 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enghitalo/vanilla/HEAD/logo.png
--------------------------------------------------------------------------------
/examples/hexagonal/src/infrastructure/database/database.v:
--------------------------------------------------------------------------------
1 | module database
2 |
--------------------------------------------------------------------------------
/examples/etag/simple:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enghitalo/vanilla/HEAD/examples/etag/simple
--------------------------------------------------------------------------------
/examples/etag/front-end/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enghitalo/vanilla/HEAD/examples/etag/front-end/favicon.ico
--------------------------------------------------------------------------------
/examples/sse/front-end/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enghitalo/vanilla/HEAD/examples/sse/front-end/favicon.ico
--------------------------------------------------------------------------------
/examples/io_uring_demo/io_uring_demo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enghitalo/vanilla/HEAD/examples/io_uring_demo/io_uring_demo
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | end_of_line = lf
4 | insert_final_newline = true
5 | trim_trailing_whitespace = true
6 |
7 | [*.v]
8 | indent_style = tab
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | *.bat eol=crlf
3 |
4 | *.v linguist-language=V
5 | *.vv linguist-language=V
6 | *.vsh linguist-language=V
7 | v.mod linguist-language=V
8 | .vdocignore linguist-language=ignore
9 |
--------------------------------------------------------------------------------
/v.mod:
--------------------------------------------------------------------------------
1 | Module {
2 | name: 'vanilla'
3 | description: 'Lightweight, fast and not magic Web Server'
4 | version: '0.0.1'
5 | license: 'MIT'
6 | repo_url: 'https://github.com/enghitalo/vanilla'
7 | dependencies: []
8 | }
9 |
--------------------------------------------------------------------------------
/http_server/http2/hpack.v:
--------------------------------------------------------------------------------
1 | module http2
2 |
3 | // HPACK header compression/decompression stubs (RFC 7541)
4 | // TODO: Implement full HPACK encoder/decoder
5 |
6 | pub struct HpackDecoder {}
7 |
8 | pub struct HpackEncoder {}
9 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/domain/auth_service.v:
--------------------------------------------------------------------------------
1 | module domain
2 |
3 | pub struct AuthCredentials {
4 | pub:
5 | username string
6 | password string
7 | }
8 |
9 | pub interface AuthService {
10 | authenticate(credentials AuthCredentials) !User
11 | }
12 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/domain/product_repository.v:
--------------------------------------------------------------------------------
1 | module domain
2 |
3 | pub struct Product {
4 | pub:
5 | id string
6 | name string
7 | price f64
8 | }
9 |
10 | pub interface ProductRepository {
11 | find_by_id(id string) !Product
12 | create(product Product) !Product
13 | list() ![]Product
14 | }
15 |
--------------------------------------------------------------------------------
/examples/sse/README.md:
--------------------------------------------------------------------------------
1 | ### Run the SSE server
2 |
3 | ```sh
4 | v -prod run examples/sse
5 | ```
6 |
7 | ### Serve the front-end
8 |
9 | ```sh
10 | v -e 'import net.http.file; file.serve(folder: "examples/sse/front-end")'
11 | ```
12 |
13 | ### Send notification
14 |
15 | ```sh
16 | curl -X POST -v http://localhost:3001/notification
17 | ```
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | main
3 | vanilla
4 | *.exe
5 | *.exe~
6 | *.so
7 | *.dylib
8 | *.dll
9 |
10 | # Ignore binary output folders
11 | bin/
12 |
13 | # Ignore common editor/system specific metadata
14 | .DS_Store
15 | .idea/
16 | .vscode/
17 | *.iml
18 |
19 | # ENV
20 | .env
21 |
22 | # vweb and database
23 | *.db
24 | *.js
25 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/domain/user_repository.v:
--------------------------------------------------------------------------------
1 | module domain
2 |
3 | pub struct User {
4 | pub:
5 | id string
6 | username string
7 | email string
8 | password string // hashed
9 | }
10 |
11 | pub interface UserRepository {
12 | find_by_id(id string) !User
13 | find_by_username(username string) !User
14 | create(user User) !User
15 | list() ![]User
16 | }
17 |
--------------------------------------------------------------------------------
/examples/database/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | postgres:
5 | image: postgres:16
6 | container_name: postgres
7 | environment:
8 | POSTGRES_USER: username
9 | POSTGRES_PASSWORD: password
10 | POSTGRES_DB: example
11 | ports:
12 | - "5435:5432"
13 | volumes:
14 | - postgres_data:/var/lib/postgresql/data
15 |
16 | volumes:
17 | postgres_data:
--------------------------------------------------------------------------------
/examples/hexagonal/src/infrastructure/repositories/dummy_product_repository.v:
--------------------------------------------------------------------------------
1 | module repositories
2 |
3 | import domain
4 |
5 | pub struct DummyProductRepository {}
6 |
7 | pub fn (repo DummyProductRepository) find_by_id(id string) !domain.Product {
8 | return error('not found')
9 | }
10 |
11 | pub fn (repo DummyProductRepository) create(product domain.Product) !domain.Product {
12 | return product
13 | }
14 |
15 | pub fn (repo DummyProductRepository) list() ![]domain.Product {
16 | return []domain.Product{}
17 | }
18 |
--------------------------------------------------------------------------------
/examples/sse/front-end/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SSE Notification Test
5 |
6 |
7 |
8 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.github/workflows/pretty_fmt_checker.yml:
--------------------------------------------------------------------------------
1 | name: Pretty Fmt Checker
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | fmt-check:
11 | name: v fmt -verify
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Set up V
16 | uses: vlang/setup-v@v1.4
17 | - name: Remove Vlang folder to avoid conflicts
18 | shell: bash
19 | run: |
20 | rm -rf -v ./vlang || true
21 | - name: Run v fmt checker
22 | shell: bash
23 | run: |
24 | v fmt -verify .
25 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/application/auth_usecase.v:
--------------------------------------------------------------------------------
1 | module application
2 |
3 | import domain
4 |
5 | pub struct AuthUseCase {
6 | service domain.AuthService
7 | }
8 |
9 | pub fn new_auth_usecase(service domain.AuthService) AuthUseCase {
10 | return AuthUseCase{
11 | service: service
12 | }
13 | }
14 |
15 | // pub fn (a AuthUseCase) login(username string, password string) (?domain.User, ?IError) {
16 | pub fn (a AuthUseCase) login(username string, password string) ?domain.User {
17 | credentials := domain.AuthCredentials{
18 | username: username
19 | password: password
20 | }
21 | return a.service.authenticate(credentials) or { return none }
22 | }
23 |
--------------------------------------------------------------------------------
/http_server/tls/tls.v:
--------------------------------------------------------------------------------
1 | module tls
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | fn C.mbedtls_ssl_get_ciphersuite(ssl &mbedtls_ssl_context) &char
15 | fn C.mbedtls_ssl_get_version(ssl &mbedtls_ssl_context) &char
16 | fn C.mbedtls_x509_crt_info(buf &char, size usize, prefix &char, crt &mbedtls_x509_crt) int
17 | fn C.mbedtls_strerror(errnum int, buf &u8, buflen usize)
18 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/application/product_usecase.v:
--------------------------------------------------------------------------------
1 | module application
2 |
3 | import domain
4 |
5 | pub struct ProductUseCase {
6 | repo domain.ProductRepository
7 | }
8 |
9 | pub fn new_product_usecase(repo domain.ProductRepository) ProductUseCase {
10 | return ProductUseCase{
11 | repo: repo
12 | }
13 | }
14 |
15 | pub fn (p ProductUseCase) add_product(name string, price f64) !domain.Product {
16 | product := domain.Product{
17 | id: '' // generate UUID in infra
18 | name: name
19 | price: price
20 | }
21 | return p.repo.create(product)
22 | }
23 |
24 | pub fn (p ProductUseCase) list_products() ![]domain.Product {
25 | return p.repo.list()
26 | }
27 |
--------------------------------------------------------------------------------
/examples/io_uring_demo/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 |
5 | fn handle_request(req_buffer []u8, client_conn_fd int) ![]u8 {
6 | // Simple request handler that returns OK response
7 | res := 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: keep-alive\r\n\r\nHello, World!'.bytes()
8 | return res
9 | }
10 |
11 | fn main() {
12 | // println('Starting server with ${io_multiplexing} io_multiplexing...')
13 |
14 | mut server := http_server.new_server(http_server.ServerConfig{
15 | port: 3000
16 | io_multiplexing: unsafe { http_server.IOBackend(0) }
17 | request_handler: handle_request
18 | })!
19 |
20 | server.run()
21 | }
22 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/infrastructure/http/middleware.v:
--------------------------------------------------------------------------------
1 | module http
2 |
3 | import domain
4 |
5 | pub struct SimpleAuthService {
6 | repo domain.UserRepository
7 | }
8 |
9 | pub fn new_simple_auth_service(repo domain.UserRepository) SimpleAuthService {
10 | return SimpleAuthService{
11 | repo: repo
12 | }
13 | }
14 |
15 | pub fn (a SimpleAuthService) authenticate(credentials domain.AuthCredentials) !domain.User {
16 | user := a.repo.find_by_username(credentials.username) or {
17 | return error('Authentication failed')
18 | }
19 | // In production, use a secure password hash check
20 | if user.password == credentials.password {
21 | return user
22 | }
23 | return error('Authentication failed')
24 | }
25 |
--------------------------------------------------------------------------------
/http_server/tls/tls_cert/tls_cert.v:
--------------------------------------------------------------------------------
1 | module tls_cert
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | fn C.mbedtls_pk_wrap_psa(pk &mbedtls_pk_context, key_id u32) int
14 | fn C.psa_generate_random(output &u8, output_size usize) psa_status_t
15 | fn C.psa_destroy_key(key_id u32) psa_status_t
16 |
17 | fn C.psa_crypto_init() int
18 | fn C.mbedtls_psa_crypto_free()
19 |
20 | pub const cert_buf_size = 4096
21 |
22 | @[typedef]
23 | struct C.mbedtls_x509write_cert {}
24 |
25 | @[typedef]
26 | struct C.mbedtls_pk_context {}
27 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/application/user_usecase.v:
--------------------------------------------------------------------------------
1 | module application
2 |
3 | import domain
4 |
5 | pub struct UserUseCase {
6 | repo domain.UserRepository
7 | }
8 |
9 | pub fn new_user_usecase(repo domain.UserRepository) UserUseCase {
10 | return UserUseCase{
11 | repo: repo
12 | }
13 | }
14 |
15 | pub fn (u UserUseCase) register(username string, email string, password string) !domain.User {
16 | // Hash password (placeholder, use real hash in production)
17 | hashed := password // TODO: hash
18 | user := domain.User{
19 | id: '' // generate UUID in infra
20 | username: username
21 | email: email
22 | password: hashed
23 | }
24 | return u.repo.create(user)
25 | }
26 |
27 | pub fn (u UserUseCase) list_users() ![]domain.User {
28 | return u.repo.list()
29 | }
30 |
--------------------------------------------------------------------------------
/examples/simple/src/main_test.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server.http1_1.response
4 |
5 | fn test_simple_without_init_the_server() {
6 | request1 := 'GET / HTTP/1.1\r\n\r\n'.bytes()
7 | request2 := 'GET /user/123 HTTP/1.1\r\n\r\n'.bytes()
8 | request3 := 'POST /user HTTP/1.1\r\nContent-Length: 0\r\n\r\n'.bytes()
9 | request4 := 'INVALID / HTTP/1.1\r\n\r\n'.bytes()
10 |
11 | request2_response := 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\nConnection: keep-alive\r\n\r\n123'.bytes()
12 |
13 | assert handle_request(request1, -1)! == http_ok_response
14 | assert handle_request(request2, -1)! == request2_response
15 | assert handle_request(request3, -1)! == http_created_response
16 | assert handle_request(request4, -1)! == response.tiny_bad_request_response
17 | }
18 |
--------------------------------------------------------------------------------
/examples/simple3/src/main_test.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server.http1_1.response
4 |
5 | fn test_simple_without_init_the_server() {
6 | request1 := 'GET / HTTP/1.1\r\n\r\n'.bytes()
7 | request2 := 'GET /user/123 HTTP/1.1\r\n\r\n'.bytes()
8 | request3 := 'POST /user HTTP/1.1\r\nContent-Length: 0\r\n\r\n'.bytes()
9 | request4 := 'INVALID / HTTP/1.1\r\n\r\n'.bytes()
10 |
11 | request2_response := 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\nConnection: keep-alive\r\n\r\n123'.bytes()
12 |
13 | app := App{}
14 |
15 | assert app.handle_request(request1, -1)! == http_ok_response
16 | assert app.handle_request(request2, -1)! == request2_response
17 | assert app.handle_request(request3, -1)! == http_created_response
18 | assert app.handle_request(request4, -1)! == response.tiny_bad_request_response
19 | }
20 |
--------------------------------------------------------------------------------
/http_server/http_server_linux.c.v:
--------------------------------------------------------------------------------
1 | module http_server
2 |
3 | // Backend selection
4 | pub enum IOBackend {
5 | epoll = 0 // Linux only
6 | io_uring = 1 // Linux only
7 | }
8 |
9 | const connection_keep_alive_variants = [
10 | 'Connection: keep-alive'.bytes(),
11 | 'connection: keep-alive'.bytes(),
12 | 'Connection: "keep-alive"'.bytes(),
13 | 'connection: "keep-alive"'.bytes(),
14 | 'Connection: Keep-Alive'.bytes(),
15 | 'connection: Keep-Alive'.bytes(),
16 | 'Connection: "Keep-Alive"'.bytes(),
17 | 'connection: "Keep-Alive"'.bytes(),
18 | ]!
19 |
20 | pub fn (mut server Server) run() {
21 | match server.io_multiplexing {
22 | .epoll {
23 | run_epoll_backend(server.socket_fd, server.request_handler, server.port, mut
24 | server.threads)
25 | }
26 | .io_uring {
27 | run_io_uring_backend(server.request_handler, server.port, mut server.threads)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/io_uring_demo/probe.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | int main() {
6 | struct io_uring ring;
7 | struct io_uring_probe *p;
8 |
9 | if (io_uring_queue_init(2, &ring, 0)) {
10 | perror("io_uring_queue_init");
11 | return 1;
12 | }
13 |
14 | p = io_uring_get_probe_ring(&ring);
15 | if (!p) {
16 | puts("failed to get probe");
17 | return 1;
18 | }
19 |
20 | for (unsigned i = 0; i < p->ops_len; i++) {
21 | struct io_uring_probe_op *op = &p->ops[i];
22 |
23 | if (op->op == IORING_OP_ACCEPT) {
24 | /* bit 0x1 == supports multishot (kernel ABI) */
25 | if (op->flags & 0x1)
26 | puts("multishot accept supported");
27 | else
28 | puts("NO multishot accept");
29 | }
30 | }
31 |
32 | io_uring_free_probe(p);
33 | io_uring_queue_exit(&ring);
34 | return 0;
35 | }
36 |
--------------------------------------------------------------------------------
/http_server/http1_1/request/request.c.v:
--------------------------------------------------------------------------------
1 | module request
2 |
3 | #include
4 |
5 | fn C.recv(__fd int, __buf voidptr, __n usize, __flags int) int
6 |
7 | // Request reading and handling.
8 |
9 | pub fn read_request(client_fd int) ![]u8 {
10 | mut request_buffer := []u8{}
11 | defer {
12 | if unsafe { request_buffer.len == 0 } {
13 | unsafe { request_buffer.free() }
14 | }
15 | }
16 | mut temp_buffer := [140]u8{}
17 |
18 | for {
19 | bytes_read := C.recv(client_fd, &temp_buffer[0], temp_buffer.len, 0)
20 | if bytes_read < 0 {
21 | if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
22 | break
23 | }
24 | return error('recv failed')
25 | }
26 | if bytes_read == 0 {
27 | return error('client closed connection')
28 | }
29 | unsafe { request_buffer.push_many(&temp_buffer[0], bytes_read) }
30 | if bytes_read < temp_buffer.len {
31 | break
32 | }
33 | }
34 |
35 | if request_buffer.len == 0 {
36 | return error('empty request')
37 | }
38 |
39 | return request_buffer
40 | }
41 |
--------------------------------------------------------------------------------
/examples/simple2/src/main_test.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 |
6 | fn test_handle_request_get_home() {
7 | req_buffer := 'GET / HTTP/1.1\r\n\r\n'.bytes()
8 | res := handle_request(req_buffer, -1) or { panic(err) }
9 | assert res == http_ok_response
10 | }
11 |
12 | fn test_handle_request_get_user() {
13 | req_buffer := 'GET /user/123 HTTP/1.1\r\n\r\n'.bytes()
14 | res := handle_request(req_buffer, -1) or { panic(err) }
15 | assert res == 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\nConnection: keep-alive\r\n\r\n123'.bytes()
16 | }
17 |
18 | fn test_handle_request_post_user() {
19 | req_buffer := 'POST /user HTTP/1.1\r\nContent-Length: 0\r\n\r\n'.bytes()
20 | res := handle_request(req_buffer, -1) or { panic(err) }
21 | assert res == http_created_response
22 | }
23 |
24 | fn test_handle_request_bad_request() {
25 | req_buffer := 'INVALID / HTTP/1.1\r\n\r\n'.bytes()
26 | res := handle_request(req_buffer, -1) or { panic(err) }
27 | assert res == response.tiny_bad_request_response
28 | }
29 |
--------------------------------------------------------------------------------
/examples/simple/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 | import http_server.http1_1.request_parser
6 |
7 | fn handle_request(req_buffer []u8, client_conn_fd int) ![]u8 {
8 | req := request_parser.decode_http_request(req_buffer)!
9 |
10 | method := unsafe { tos(&req.buffer[req.method.start], req.method.len) }
11 | path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
12 |
13 | if method == 'GET' {
14 | if path == '/' {
15 | return home_controller([])
16 | } else if path.starts_with('/user/') {
17 | id := path[6..]
18 | return get_user_controller([id])
19 | }
20 | } else if method == 'POST' {
21 | if path == '/user' {
22 | return create_user_controller([])
23 | }
24 | }
25 |
26 | return response.tiny_bad_request_response
27 | }
28 |
29 | fn main() {
30 | mut server := http_server.new_server(http_server.ServerConfig{
31 | port: 3000
32 | request_handler: handle_request
33 | io_multiplexing: unsafe { http_server.IOBackend.io_uring }
34 | })!
35 |
36 | server.run()
37 | }
38 |
--------------------------------------------------------------------------------
/examples/etag/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 | import http_server.http1_1.request_parser
6 |
7 | fn handle_request(req_buffer []u8, client_conn_fd int) ![]u8 {
8 | req := request_parser.decode_http_request(req_buffer)!
9 |
10 | method := unsafe { tos(&req.buffer[req.method.start], req.method.len) }
11 | path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
12 |
13 | if method == 'GET' {
14 | if path == '/' {
15 | return home_controller([])
16 | } else if path.starts_with('/user/') {
17 | id := path[6..]
18 |
19 | return get_user_controller([id], req)
20 | }
21 | } else if method == 'POST' {
22 | if path == '/user' {
23 | return create_user_controller([])
24 | }
25 | }
26 |
27 | return response.tiny_bad_request_response
28 | }
29 |
30 | fn main() {
31 | mut server := http_server.new_server(http_server.ServerConfig{
32 | port: 3000
33 | io_multiplexing: unsafe { http_server.IOBackend(0) }
34 | request_handler: handle_request
35 | })!
36 |
37 | server.run()
38 | }
39 |
--------------------------------------------------------------------------------
/examples/etag/src/main_test.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server.http1_1.response
4 |
5 | fn test_handle_request_get_home() {
6 | req_buffer := 'GET / HTTP/1.1\r\n\r\n'.bytes()
7 | res := handle_request(req_buffer, -1) or { panic(err) }
8 | assert res == http_ok_response
9 | }
10 |
11 | fn test_handle_request_get_user() {
12 | req_buffer := 'GET /user/123 HTTP/1.1\r\n\r\n'.bytes()
13 | res := handle_request(req_buffer, -1) or { panic(err) }
14 | assert res.bytestr() == 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nETag: 202cb962ac59075b964b07152d234b70\r\nContent-Length: 3\r\nAccess-Control-Allow-Origin: *\r\n\r\n123'
15 | }
16 |
17 | fn test_handle_request_post_user() {
18 | req_buffer := 'POST /user HTTP/1.1\r\nContent-Length: 0\r\n\r\n'.bytes()
19 | res := handle_request(req_buffer, -1) or { panic(err) }
20 | assert res == http_created_response
21 | }
22 |
23 | fn test_handle_request_bad_request() {
24 | req_buffer := 'INVALID / HTTP/1.1\r\n\r\n'.bytes()
25 | res := handle_request(req_buffer, -1) or { panic(err) }
26 | assert res == response.tiny_bad_request_response
27 | }
28 |
--------------------------------------------------------------------------------
/http_server/http1_1/response/response.c.v:
--------------------------------------------------------------------------------
1 | module response
2 |
3 | #include
4 |
5 | fn C.send(__fd int, __buf voidptr, __n usize, __flags int) int
6 | fn C.perror(s &u8)
7 |
8 | pub const tiny_bad_request_response = 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
9 | const status_444_response = 'HTTP/1.1 444 No Response\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
10 |
11 | // HTTP response helpers.
12 |
13 | pub fn send_response(fd int, buffer_ptr &u8, buffer_len int) ! {
14 | flags := $if linux {
15 | C.MSG_NOSIGNAL | C.MSG_ZEROCOPY
16 | } $else {
17 | C.MSG_NOSIGNAL
18 | }
19 | sent := C.send(fd, buffer_ptr, buffer_len, flags)
20 | if sent < 0 && C.errno != C.EAGAIN && C.errno != C.EWOULDBLOCK {
21 | eprintln(@LOCATION)
22 | C.perror(c'send')
23 | return error('send failed')
24 | }
25 | }
26 |
27 | pub fn send_bad_request_response(fd int) {
28 | C.send(fd, tiny_bad_request_response.data, tiny_bad_request_response.len, 0)
29 | }
30 |
31 | pub fn send_status_444_response(fd int) {
32 | C.send(fd, status_444_response.data, status_444_response.len, 0)
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Hitalo Souza
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/http_server/http2/frame.v:
--------------------------------------------------------------------------------
1 | module http2
2 |
3 | // HTTP/2 frame parsing and serialization (RFC 9113 Section 4.1)
4 |
5 | pub fn parse_frame_header(data []u8) !FrameHeader {
6 | if data.len < 9 {
7 | return error('Frame header too short')
8 | }
9 | length := (u32(data[0]) << 16) | (u32(data[1]) << 8) | u32(data[2])
10 | type_ := unsafe { FrameType(data[3]) }
11 | flags := data[4]
12 | mut stream_id := (u32(data[5]) << 24) | (u32(data[6]) << 16) | (u32(data[7]) << 8) | u32(data[8])
13 | stream_id &= 0x7FFFFFFF // clear the reserved bit
14 | return FrameHeader{
15 | length: length
16 | type_: type_
17 | flags: flags
18 | stream_id: stream_id
19 | }
20 | }
21 |
22 | pub fn serialize_frame_header(header FrameHeader) []u8 {
23 | mut data := []u8{len: 9}
24 | data[0] = u8((header.length >> 16) & 0xFF)
25 | data[1] = u8((header.length >> 8) & 0xFF)
26 | data[2] = u8(header.length & 0xFF)
27 | data[3] = u8(header.type_)
28 | data[4] = header.flags
29 | data[5] = u8((header.stream_id >> 24) & 0x7F) // reserved bit is 0
30 | data[6] = u8((header.stream_id >> 16) & 0xFF)
31 | data[7] = u8((header.stream_id >> 8) & 0xFF)
32 | data[8] = u8(header.stream_id & 0xFF)
33 | return data
34 | }
35 |
--------------------------------------------------------------------------------
/examples/database/src/database.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import db.pg
4 |
5 | pub struct ConnectionPool {
6 | mut:
7 | connections chan pg.DB
8 | config pg.Config
9 | }
10 |
11 | // new_connection_pool creates a new connection pool with the given size and configuration.
12 | pub fn new_connection_pool(config pg.Config, size int) !ConnectionPool {
13 | mut connections := chan pg.DB{cap: size}
14 | for _ in 0 .. size {
15 | conn := pg.connect(config)!
16 | connections <- conn
17 | }
18 | return ConnectionPool{
19 | connections: connections
20 | config: config
21 | }
22 | }
23 |
24 | // acquire gets a connection from the pool
25 | pub fn (mut pool ConnectionPool) acquire() !pg.DB {
26 | return <-pool.connections or { return error('Failed to acquire a connection from the pool') }
27 | }
28 |
29 | // release returns a connection back to the pool.
30 | pub fn (mut pool ConnectionPool) release(conn pg.DB) {
31 | pool.connections <- conn
32 | }
33 |
34 | // close closes all connections in the pool.
35 | pub fn (mut pool ConnectionPool) close() {
36 | for _ in 0 .. pool.connections.len {
37 | mut conn := <-pool.connections or { break }
38 | conn.close() or { eprintln('Failed to close connection: ${err}') }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/simple/src/server_end_to_end_test.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 |
6 | fn test_server_end_to_end() ! {
7 | // Prepare requests
8 | request1 := 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
9 | request2 := 'GET /user/123 HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
10 | request3 := 'POST /user HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n'.bytes()
11 | request4 := 'INVALID / HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
12 | requests := [request1, request2, request3, request4]
13 |
14 | mut server := http_server.new_server(http_server.ServerConfig{
15 | port: 8082
16 | request_handler: handle_request
17 | io_multiplexing: unsafe { http_server.IOBackend(0) }
18 | })!
19 | responses := server.test(requests) or { panic('[test] server.test failed: ${err}') }
20 | assert responses.len == 4
21 | assert responses[0] == http_ok_response
22 | assert responses[1] == 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\nConnection: keep-alive\r\n\r\n123'.bytes()
23 | assert responses[2] == http_created_response
24 | assert responses[3] == response.tiny_bad_request_response
25 | println('[test] test_server_end_to_end passed!')
26 | }
27 |
--------------------------------------------------------------------------------
/examples/simple3/src/server_end_to_end_test.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 |
6 | fn test_server_end_to_end() ! {
7 | // Prepare requests
8 | request1 := 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
9 | request2 := 'GET /user/123 HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
10 | request3 := 'POST /user HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n'.bytes()
11 | request4 := 'INVALID / HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
12 | requests := [request1, request2, request3, request4]
13 |
14 | app := App{}
15 |
16 | mut server := http_server.new_server(http_server.ServerConfig{
17 | port: 8082
18 | request_handler: app.handle_request
19 | io_multiplexing: unsafe { http_server.IOBackend(0) }
20 | })!
21 | responses := server.test(requests) or { panic('[test] server.test failed: ${err}') }
22 | assert responses.len == 4
23 | assert responses[0] == http_ok_response
24 | assert responses[1] == 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\nConnection: keep-alive\r\n\r\n123'.bytes()
25 | assert responses[2] == http_created_response
26 | assert responses[3] == response.tiny_bad_request_response
27 | println('[test] test_server_end_to_end passed!')
28 | }
29 |
--------------------------------------------------------------------------------
/examples/simple/src/controllers.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import strings
4 | import http_server.http1_1.response
5 |
6 | const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
7 |
8 | const http_created_response = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
9 |
10 | fn home_controller(params []string) ![]u8 {
11 | return http_ok_response
12 | }
13 |
14 | fn get_users_controller(params []string) ![]u8 {
15 | return http_ok_response
16 | }
17 |
18 | @[direct_array_access; manualfree]
19 | fn get_user_controller(params []string) ![]u8 {
20 | if params.len == 0 {
21 | return response.tiny_bad_request_response
22 | }
23 | id := params[0]
24 | response_body := id
25 |
26 | mut sb := strings.new_builder(200)
27 | sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ')
28 | sb.write_string(response_body.len.str())
29 | sb.write_string('\r\nConnection: keep-alive\r\n\r\n')
30 | sb.write_string(response_body)
31 |
32 | defer {
33 | unsafe {
34 | response_body.free()
35 | params.free()
36 | }
37 | }
38 | return sb
39 | }
40 |
41 | fn create_user_controller(params []string) ![]u8 {
42 | return http_created_response
43 | }
44 |
--------------------------------------------------------------------------------
/examples/simple2/src/controllers.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import strings
4 | import http_server.http1_1.response
5 |
6 | const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
7 |
8 | const http_created_response = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
9 |
10 | fn home_controller(params []string) ![]u8 {
11 | return http_ok_response
12 | }
13 |
14 | fn get_users_controller(params []string) ![]u8 {
15 | return http_ok_response
16 | }
17 |
18 | @[direct_array_access; manualfree]
19 | fn get_user_controller(params []string) ![]u8 {
20 | if params.len == 0 {
21 | return response.tiny_bad_request_response
22 | }
23 | id := params[0]
24 | response_body := id
25 |
26 | mut sb := strings.new_builder(200)
27 | sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ')
28 | sb.write_string(response_body.len.str())
29 | sb.write_string('\r\nConnection: keep-alive\r\n\r\n')
30 | sb.write_string(response_body)
31 |
32 | defer {
33 | unsafe {
34 | response_body.free()
35 | params.free()
36 | }
37 | }
38 | return sb
39 | }
40 |
41 | fn create_user_controller(params []string) ![]u8 {
42 | return http_created_response
43 | }
44 |
--------------------------------------------------------------------------------
/examples/simple2/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.request_parser
5 | import http_server.http1_1.response
6 |
7 | fn handle_request(req_buffer []u8, client_conn_fd int) ![]u8 {
8 | req := request_parser.decode_http_request(req_buffer)!
9 | method := req.method.to_string(req.buffer)
10 | path := req.path.to_string(req.buffer)
11 |
12 | match method {
13 | 'GET' {
14 | match path {
15 | '/' {
16 | return home_controller([])
17 | }
18 | '/users' {
19 | return get_users_controller([])
20 | }
21 | else {
22 | if path.starts_with('/user/') {
23 | id := path[6..]
24 | return get_user_controller([id])
25 | }
26 | return response.tiny_bad_request_response
27 | }
28 | }
29 | }
30 | 'POST' {
31 | if path == '/user' {
32 | return create_user_controller([])
33 | }
34 | return response.tiny_bad_request_response
35 | }
36 | else {
37 | return response.tiny_bad_request_response
38 | }
39 | }
40 |
41 | return response.tiny_bad_request_response
42 | }
43 |
44 | fn main() {
45 | mut server := http_server.new_server(http_server.ServerConfig{
46 | port: 3000
47 | io_multiplexing: unsafe { http_server.IOBackend(0) }
48 | request_handler: handle_request
49 | })!
50 | server.run()
51 | }
52 |
--------------------------------------------------------------------------------
/.github/workflows/build_test_examples_on_darwin.yml:
--------------------------------------------------------------------------------
1 | name: (MacOS) Build & Test Examples
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | examples-building-and-testing:
11 | runs-on: macos-latest
12 | name: ${{ matrix.folder }}
13 | strategy:
14 | fail-fast: false # Continue other jobs if one fails
15 | matrix:
16 | folder:
17 | - examples/database/src
18 | - examples/etag/src
19 | - examples/hexagonal/src
20 | - examples/io_uring_demo/src
21 | # - examples/simple/src
22 | - examples/simple2/src
23 | - examples/sse/src
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: Set up V
27 | uses: vlang/setup-v@v1.4
28 | - name: Remove Vlang folder to avoid conflicts
29 | shell: bash
30 | run: |
31 | rm -rf -v ./vlang || true
32 |
33 | - name: Install dependencies
34 | shell: bash
35 | run: |
36 | brew update
37 | brew install sqlite3
38 | - name: Build ${{ matrix.folder }}
39 | shell: bash
40 | run: |
41 | v $(pwd)/${{ matrix.folder }}
42 | - name: Test ${{ matrix.folder }}
43 | shell: bash
44 | run: |
45 | v test $(pwd)/${{ matrix.folder }}
46 |
--------------------------------------------------------------------------------
/.github/workflows/build_test_examples_on_windows.yml:
--------------------------------------------------------------------------------
1 | name: (Windows) Build & Test Examples
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | examples-building-and-testing:
11 | runs-on: windows-latest
12 | name: ${{ matrix.folder }}
13 | strategy:
14 | fail-fast: false # Continue other jobs if one fails
15 | matrix:
16 | folder:
17 | - examples/database/src
18 | - examples/etag/src
19 | - examples/hexagonal/src
20 | - examples/io_uring_demo/src
21 | - examples/simple/src
22 | - examples/simple2/src
23 | - examples/sse/src
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: Set up V
27 | uses: vlang/setup-v@v1.4
28 | - name: Remove Vlang folder to avoid conflicts
29 | shell: bash
30 | run: |
31 | rm -rfv vlang || true
32 |
33 | - name: Install dependencies
34 | shell: bash
35 | run: |
36 | echo "No additional dependencies to install on Windows."
37 |
38 | - name: Build ${{ matrix.folder }}
39 | shell: bash
40 | run: |
41 | v "${{ matrix.folder }}"
42 | - name: Test ${{ matrix.folder }}
43 | shell: bash
44 | run: |
45 | v test "${{ matrix.folder }}"
46 |
--------------------------------------------------------------------------------
/http_server/server_test.v:
--------------------------------------------------------------------------------
1 | module http_server
2 |
3 | import os
4 |
5 | fn dummy_handler(req []u8, _ int) ![]u8 {
6 | if req.bytestr().contains('/notfound') {
7 | return 'HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found'.bytes()
8 | }
9 | return 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK'.bytes()
10 | }
11 |
12 | fn test_server_end_to_end() ! {
13 | println('[test] Using in-memory request buffers...')
14 | request1 := 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
15 | request2 := 'GET /notfound HTTP/1.1\r\nHost: localhost\r\n\r\n'.bytes()
16 | requests := [request1, request2]
17 |
18 | println('[test] Creating server...')
19 | mut server := new_server(ServerConfig{
20 | port: 8081
21 | io_multiplexing: unsafe { IOBackend(0) }
22 | request_handler: dummy_handler
23 | })!
24 | println('[test] Running server.test...')
25 | responses := server.test(requests) or {
26 | eprintln('[test] server.test failed: ${err}')
27 | return err
28 | }
29 | println('[test] Got ${responses.len} responses')
30 | assert responses.len == 2
31 | println('[test] Response 1: ' + responses[0].bytestr())
32 | println('[test] Response 2: ' + responses[1].bytestr())
33 | assert responses[0].bytestr().contains('200 OK')
34 | assert responses[1].bytestr().contains('404 Not Found')
35 | println('[test] test_server_end_to_end passed!')
36 | }
37 |
--------------------------------------------------------------------------------
/examples/etag/README.md:
--------------------------------------------------------------------------------
1 | ## Running the Server
2 |
3 | To run the example server in production mode, use the following command:
4 |
5 | ```sh
6 | v -prod run examples/etag
7 | ```
8 |
9 | ### Serving the Front-End
10 |
11 | To serve the front-end files, execute:
12 |
13 | ```sh
14 | v -e 'import net.http.file; file.serve(folder: "examples/etag/front-end")'
15 | ```
16 |
17 | ### Testing with ETag
18 |
19 | You can test the server's ETag functionality using `curl`:
20 |
21 | 1. Fetch a resource:
22 |
23 | ```sh
24 | curl -v http://localhost:3001/user/1
25 | ```
26 |
27 | 2. Test with a specific ETag:
28 | ```sh
29 | curl -v -H "If-None-Match: c4ca4238a0b923820dcc509a6f75849b" http://localhost:3001/user/1
30 | ```
31 |
32 | ## Benchmarking
33 |
34 | - use `-d force_keep_alive` to make sure that client will not be "blocked" by having to create a new connection at each request
35 |
36 | ### Benchmarking with `wrk`
37 |
38 | You can benchmark the server's performance using `wrk`. For example:
39 |
40 | ```sh
41 | wrk -t16 -c512 -d30s http://localhost:3001/user/1
42 | ```
43 |
44 | ### Benchmarking with ETag Header
45 |
46 | To benchmark the server's performance while including an ETag header, use the following `wrk` command:
47 |
48 | ```sh
49 | wrk -t16 -c512 -d30s -H "If-None-Match: c4ca4238a0b923820dcc509a6f75849b" http://localhost:3001/user/1
50 | ```
51 |
--------------------------------------------------------------------------------
/.github/workflows/build_test_examples_on_linux.yml:
--------------------------------------------------------------------------------
1 | name: (Linux) Build & Test Examples
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | examples-building-and-testing:
11 | runs-on: ubuntu-latest
12 | name: ${{ matrix.folder }}
13 | strategy:
14 | fail-fast: false # Continue other jobs if one fails
15 | matrix:
16 | folder:
17 | - examples/database/src
18 | - examples/etag/src
19 | - examples/hexagonal/src
20 | - examples/io_uring_demo/src
21 | - examples/simple/src
22 | - examples/simple2/src
23 | - examples/sse/src
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: Set up V
27 | uses: vlang/setup-v@v1.4
28 | - name: Remove Vlang folder to avoid conflicts
29 | shell: bash
30 | run: |
31 | rm -rf -v ./vlang || true
32 |
33 | - name: Install dependencies
34 | shell: bash
35 | run: |
36 | sudo apt-get update
37 | sudo apt-get install -y libsqlite3-dev liburing-dev linux-libc-dev
38 | - name: Build ${{ matrix.folder }}
39 | shell: bash
40 | run: |
41 | v $(pwd)/${{ matrix.folder }}
42 | - name: Test ${{ matrix.folder }}
43 | shell: bash
44 | run: |
45 | v test $(pwd)/${{ matrix.folder }}
46 |
--------------------------------------------------------------------------------
/http_server/http2/types.v:
--------------------------------------------------------------------------------
1 | module http2
2 |
3 | // HTTP/2 frame types (RFC 9113 Section 6)
4 | pub enum FrameType {
5 | data = 0x0
6 | headers = 0x1
7 | priority = 0x2
8 | rst_stream = 0x3
9 | settings = 0x4
10 | push_promise = 0x5
11 | ping = 0x6
12 | goaway = 0x7
13 | window_update = 0x8
14 | continuation = 0x9
15 | }
16 |
17 | // HTTP/2 settings identifiers (RFC 9113 Section 6.5.2)
18 | pub enum SettingsId {
19 | settings_header_table_size = 0x1
20 | settings_enable_push = 0x2
21 | settings_max_concurrent_streams = 0x3
22 | settings_initial_window_size = 0x4
23 | settings_max_frame_size = 0x5
24 | settings_max_header_list_size = 0x6
25 | }
26 |
27 | // HTTP/2 error codes (RFC 9113 Section 7)
28 | pub enum ErrorCode {
29 | no_error = 0x0
30 | protocol_error = 0x1
31 | internal_error = 0x2
32 | flow_control_error = 0x3
33 | settings_timeout = 0x4
34 | stream_closed = 0x5
35 | frame_size_error = 0x6
36 | refused_stream = 0x7
37 | cancel = 0x8
38 | compression_error = 0x9
39 | connect_error = 0xa
40 | enhance_your_calm = 0xb
41 | inadequate_security = 0xc
42 | http_1_1_required = 0xd
43 | }
44 |
45 | // HTTP/2 frame header structure
46 | pub struct FrameHeader {
47 | pub:
48 | length u32 // 24 bits
49 | type_ FrameType
50 | flags u8
51 | stream_id u32 // 31 bits
52 | }
53 |
--------------------------------------------------------------------------------
/examples/etag/front-end/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ETag Cache Demo
6 |
7 |
8 | ETag Cache Demo
9 |
10 |
11 |
12 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/examples/simple3/src/controllers.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import strings
4 | import http_server.http1_1.request_parser
5 |
6 | const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
7 |
8 | const http_created_response = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
9 |
10 | fn (controller App) home_controller(_ request_parser.HttpRequest) ![]u8 {
11 | return http_ok_response
12 | }
13 |
14 | fn (controller App) get_users_controller(_ request_parser.HttpRequest) ![]u8 {
15 | return http_ok_response
16 | }
17 |
18 | @[direct_array_access; manualfree]
19 | fn (controller App) get_user_controller(req request_parser.HttpRequest) ![]u8 {
20 | path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
21 |
22 | id := path[6..] // assuming path is like "/user/{id}"
23 | response_body := id
24 |
25 | mut sb := strings.new_builder(200)
26 | sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ')
27 | sb.write_string(response_body.len.str())
28 | sb.write_string('\r\nConnection: keep-alive\r\n\r\n')
29 | sb.write_string(response_body)
30 |
31 | defer {
32 | unsafe {
33 | response_body.free()
34 | }
35 | }
36 | return sb
37 | }
38 |
39 | fn (controller App) create_user_controller(_ request_parser.HttpRequest) ![]u8 {
40 | return http_created_response
41 | }
42 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Rules
4 |
5 | - Don't slow down performance
6 | - Always try to keep abstraction to a minimum
7 | - Don't complicate it
8 |
9 | ## Sending Raw HTTP Requests for Testing
10 |
11 | You can test your server by sending raw HTTP requests directly using tools like `nc` (netcat), `telnet`, or `socat`. This is useful for debugging, learning, or end-to-end testing.
12 |
13 | ### Using netcat (nc)
14 |
15 | ```sh
16 | printf "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" | nc localhost 3000
17 | ```
18 |
19 | Send a POST request:
20 |
21 | ```sh
22 | printf "POST /user HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" | nc localhost 3000
23 | ```
24 |
25 | ### Using socat
26 |
27 | ```sh
28 | printf "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" | socat - TCP:localhost:3000
29 | ```
30 |
31 | ### Using curl (standard requests only)
32 |
33 | ```sh
34 | curl -v http://localhost:3000/
35 | curl -X POST -v http://localhost:3000/user
36 | curl -X GET -v http://localhost:3000/user/1
37 | ```
38 |
39 | ### Benchmarking
40 |
41 | - use `-d force_keep_alive` to make sure that client will not be "blocked" by having to create a new connection at each request
42 |
43 | ## WRK
44 |
45 | ```sh
46 | wrk -H 'Connection: "keep-alive"' --connection 512 --threads 16 --duration 10s http://localhost:3000
47 | ```
48 |
49 | ### Valgrind
50 |
51 | ```sh
52 | # Race condition check
53 | v -prod -gc none .
54 | valgrind --tool=helgrind ./vanilla
55 | ```
56 |
--------------------------------------------------------------------------------
/http_server/epoll/epoll_linux.c.v:
--------------------------------------------------------------------------------
1 | module epoll
2 |
3 | #include
4 |
5 | $if !windows {
6 | #include
7 | }
8 |
9 | fn C.epoll_create1(__flags int) int
10 | fn C.epoll_ctl(__epfd int, __op int, __fd int, __event &C.epoll_event) int
11 | fn C.epoll_wait(__epfd int, __events &C.epoll_event, __maxevents int, __timeout int) int
12 | fn C.perror(s &u8)
13 | fn C.close(fd int)
14 |
15 | union C.epoll_data {
16 | ptr voidptr
17 | fd int
18 | u32 u32
19 | u64 u64
20 | }
21 |
22 | pub struct C.epoll_event {
23 | events u32
24 | data C.epoll_data
25 | }
26 |
27 | // Callbacks for epoll-driven IO events.
28 | pub struct EpollEventCallbacks {
29 | pub:
30 | on_read fn (fd int) @[required]
31 | on_write fn (fd int) @[required]
32 | }
33 |
34 | // Create a new epoll instance. Returns fd or <0 on error.
35 | pub fn create_epoll_fd() int {
36 | epoll_fd := C.epoll_create1(0)
37 | if epoll_fd < 0 {
38 | C.perror(c'epoll_create1')
39 | }
40 | return epoll_fd
41 | }
42 |
43 | // Add a file descriptor to an epoll instance with given event mask.
44 | pub fn add_fd_to_epoll(epoll_fd int, fd int, events u32) int {
45 | mut ev := C.epoll_event{
46 | events: events
47 | }
48 | ev.data.fd = fd
49 | if C.epoll_ctl(epoll_fd, C.EPOLL_CTL_ADD, fd, &ev) == -1 {
50 | eprintln(@LOCATION)
51 | C.perror(c'epoll_ctl')
52 | return -1
53 | }
54 | return 0
55 | }
56 |
57 | // Remove a file descriptor from an epoll instance.
58 | pub fn remove_fd_from_epoll(epoll_fd int, fd int) {
59 | C.epoll_ctl(epoll_fd, C.EPOLL_CTL_DEL, fd, C.NULL)
60 | C.close(fd)
61 | }
62 |
--------------------------------------------------------------------------------
/examples/simple3/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 | import http_server.http1_1.request_parser
6 | import pool
7 | import db.sqlite
8 | import time
9 |
10 | struct App {
11 | pub mut:
12 | db_pool ?pool.ConnectionPool
13 | }
14 |
15 | fn (app App) handle_request(req_buffer []u8, client_conn_fd int) ![]u8 {
16 | req := request_parser.decode_http_request(req_buffer)!
17 |
18 | method := unsafe { tos(&req.buffer[req.method.start], req.method.len) }
19 | path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
20 |
21 | if method == 'GET' {
22 | if path == '/' {
23 | return app.home_controller(req)
24 | } else if path.starts_with('/user/') {
25 | return app.get_user_controller(req)
26 | }
27 | } else if method == 'POST' {
28 | if path == '/user' {
29 | return app.create_user_controller(req)
30 | }
31 | }
32 |
33 | return response.tiny_bad_request_response
34 | }
35 |
36 | fn main() {
37 | pool_factory := fn () !&pool.ConnectionPoolable {
38 | mut db := sqlite.connect('simple.db')!
39 | return &db
40 | }
41 |
42 | db_pool := pool.new_connection_pool(pool_factory, pool.ConnectionPoolConfig{
43 | max_conns: 5
44 | min_idle_conns: 1
45 | max_lifetime: 30 * time.minute
46 | idle_timeout: 5 * time.minute
47 | get_timeout: 2 * time.second
48 | }) or { panic('Failed to create SQLite pool: ' + err.msg()) }
49 |
50 | app := App{
51 | db_pool: *db_pool
52 | }
53 |
54 | mut server := http_server.new_server(http_server.ServerConfig{
55 | port: 3000
56 | request_handler: fn [app] (req_buffer []u8, client_conn_fd int) ![]u8 {
57 | return app.handle_request(req_buffer, client_conn_fd)
58 | }
59 | io_multiplexing: unsafe { http_server.IOBackend(0) }
60 | })!
61 |
62 | server.run()
63 | }
64 |
--------------------------------------------------------------------------------
/examples/database/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 | import http_server.http1_1.request_parser
6 | import db.pg
7 |
8 | fn handle_request(req_buffer []u8, client_conn_fd int, mut pool ConnectionPool) ![]u8 {
9 | req := request_parser.decode_http_request(req_buffer)!
10 |
11 | method := unsafe { tos(&req.buffer[req.method.start], req.method.len) }
12 | path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
13 |
14 | if method == 'GET' {
15 | if path == '/' {
16 | return home_controller([])
17 | } else if path.starts_with('/user/') {
18 | id := path[6..]
19 | return get_user_controller([id], mut pool)
20 | } else if path == '/user' {
21 | return get_users_controller([], mut pool)
22 | }
23 | } else if method == 'POST' {
24 | if path == '/user' {
25 | return create_user_controller([], mut pool)
26 | }
27 | }
28 |
29 | return response.tiny_bad_request_response
30 | }
31 |
32 | fn main() {
33 | mut pool := new_connection_pool(pg.Config{
34 | host: 'localhost'
35 | port: 5435
36 | user: 'username'
37 | password: 'password'
38 | dbname: 'example'
39 | }, 5) or { panic('Failed to create pg pool: ${err}') }
40 |
41 | db := pool.acquire() or { panic(err) }
42 | db.exec('create table if not exists users (id serial primary key, name text not null)') or {
43 | panic('Failed to create table users: ${err}')
44 | }
45 | pool.release(db)
46 |
47 | // Create and run the server with the handle_request function
48 |
49 | mut server := http_server.new_server(http_server.ServerConfig{
50 | port: 3000
51 | io_multiplexing: unsafe { http_server.IOBackend(0) }
52 | request_handler: fn [mut pool] (req_buffer []u8, client_conn_fd int) ![]u8 {
53 | return handle_request(req_buffer, client_conn_fd, mut pool)
54 | }
55 | })!
56 |
57 | server.run()
58 |
59 | pool.close()
60 | }
61 |
--------------------------------------------------------------------------------
/http_server/http1_1/request_parser/request_parser_test.v:
--------------------------------------------------------------------------------
1 | module request_parser
2 |
3 | fn test_parse_http1_request_line_valid_request() {
4 | buffer := 'GET /path/to/resource HTTP/1.1\r\n'.bytes()
5 | mut req := HttpRequest{
6 | buffer: buffer
7 | }
8 |
9 | parse_http1_request_line(mut req) or { panic(err) }
10 |
11 | assert req.method.to_string(req.buffer) == 'GET'
12 | assert req.path.to_string(req.buffer) == '/path/to/resource'
13 | assert req.version.to_string(req.buffer) == 'HTTP/1.1'
14 | }
15 |
16 | fn test_parse_http1_request_line_invalid_request() {
17 | buffer := 'INVALID REQUEST LINE'.bytes()
18 | mut req := HttpRequest{
19 | buffer: buffer
20 | }
21 |
22 | mut has_error := false
23 | parse_http1_request_line(mut req) or {
24 | has_error = true
25 | assert err.msg() == 'Missing CR'
26 | }
27 | assert has_error, 'Expected error for invalid request line'
28 | }
29 |
30 | fn test_decode_http_request_valid_request() {
31 | buffer := 'POST /api/resource HTTP/1.0\r\n'.bytes()
32 | req := decode_http_request(buffer) or { panic(err) }
33 |
34 | assert req.method.to_string(req.buffer) == 'POST'
35 | assert req.path.to_string(req.buffer) == '/api/resource'
36 | assert req.version.to_string(req.buffer) == 'HTTP/1.0'
37 | }
38 |
39 | fn test_decode_http_request_invalid_request() {
40 | buffer := 'INVALID REQUEST LINE'.bytes()
41 |
42 | mut has_error := false
43 | decode_http_request(buffer) or {
44 | has_error = true
45 | assert err.msg() == 'Missing CR'
46 | }
47 | assert has_error, 'Expected error for invalid request'
48 | }
49 |
50 | fn test_get_header_value_slice_existing_header() {
51 | buffer := 'GET / HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/html\r\n\r\n'.bytes()
52 | req := decode_http_request(buffer) or { panic(err) }
53 |
54 | host_slice := req.get_header_value_slice('Host') or { panic('Header not found') }
55 | assert host_slice.to_string(req.buffer) == 'example.com'
56 |
57 | content_type_slice := req.get_header_value_slice('Content-Type') or {
58 | panic('Header not found')
59 | }
60 | assert content_type_slice.to_string(req.buffer) == 'text/html'
61 | }
62 |
63 | fn test_get_header_value_slice_non_existing_header() {
64 | buffer := 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'.bytes()
65 | req := decode_http_request(buffer) or { panic(err) }
66 |
67 | assert req.get_header_value_slice('Content-Type') == none
68 | }
69 |
--------------------------------------------------------------------------------
/examples/etag/src/controllers.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import strings
4 | import http_server.http1_1.response
5 | import http_server.http1_1.request_parser
6 | import crypto.md5
7 |
8 | const not_modified_responsense = 'HTTP/1.1 304 Not Modified\r\n\r\n'.bytes()
9 |
10 | fn generate_etag(content []u8) []u8 {
11 | return md5.sum(content)
12 | }
13 |
14 | const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\n\r\n'.bytes()
15 |
16 | const http_created_response = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 0\r\n\r\n'.bytes()
17 |
18 | fn home_controller(params []string) ![]u8 {
19 | return http_ok_response
20 | }
21 |
22 | fn get_users_controller(params []string) ![]u8 {
23 | return http_ok_response
24 | }
25 |
26 | @[direct_array_access; manualfree]
27 | fn get_user_controller(params []string, req request_parser.HttpRequest) ![]u8 {
28 | if params.len == 0 {
29 | return response.tiny_bad_request_response
30 | }
31 | id := params[0]
32 | response_body := id.bytes() // Convert to []u8 for hashing
33 |
34 | // Generate an ETag for the response body.
35 | etag := generate_etag(response_body)
36 | etag_str := etag.hex() // convert to a hexadecimal string for header usage
37 |
38 | // Extract the If-None-Match header from the request.
39 | if_none_match := req.get_header_value_slice('If-None-Match')
40 | if if_none_match != none {
41 | // Compare the provided ETag with the generated one.
42 | if unsafe { vmemcmp(&req.buffer[if_none_match.start], etag_str.str, etag_str.len) } == 0 {
43 | // If they match, return a 304 Not Modified response.
44 | return not_modified_responsense
45 | }
46 | }
47 |
48 | // Build the full response including the new ETag header.
49 | mut sb := strings.new_builder(200)
50 | body_str := unsafe { tos(response_body.data, response_body.len) }
51 |
52 | // Write the response with ETag.
53 | sb.write_string('HTTP/1.1 200 OK\r\n')
54 | sb.write_string('Content-Type: text/plain\r\n')
55 | sb.write_string('ETag: ' + etag_str + '\r\n')
56 | sb.write_string('Content-Length: ' + body_str.len.str() + '\r\n')
57 | sb.write_string('Access-Control-Allow-Origin: *\r\n\r\n')
58 | sb.write_string(body_str)
59 |
60 | defer {
61 | unsafe {
62 | response_body.free()
63 | params.free()
64 | }
65 | }
66 | return sb
67 | }
68 |
69 | fn create_user_controller(params []string) ![]u8 {
70 | return http_created_response
71 | }
72 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/infrastructure/database/db_pool.v:
--------------------------------------------------------------------------------
1 | module database
2 |
3 | import pool
4 | import db.pg
5 | import db.sqlite
6 |
7 | // Abstract DB connection type for pooling
8 | pub type DbConn = pg.DB | sqlite.DB
9 |
10 | // // ConnectionPoolable defines the interface for connection objects
11 | // pub interface ConnectionPoolable {
12 | // mut:
13 | // // validate checks if the connection is still usable
14 | // validate() !bool
15 | // // close terminates the physical connection
16 | // close() !
17 | // // reset returns the connection to initial state for reuse
18 | // reset() !
19 | // }
20 | fn (c DbConn) validate() !bool {
21 | return match c {
22 | pg.DB {
23 | // return c.ping() !
24 | }
25 | sqlite.DB {
26 | // For SQLite, we can assume the connection is always valid
27 | return true
28 | }
29 | }
30 | }
31 |
32 | fn (mut c DbConn) close() ! {
33 | return match mut c {
34 | pg.DB {
35 | return c.close()
36 | }
37 | sqlite.DB {
38 | return c.close()
39 | }
40 | }
41 | }
42 |
43 | fn (mut c DbConn) reset() ! {
44 | // No-op for now, can add logic if needed
45 | }
46 |
47 | // Pool wrapper for both backends
48 | pub struct DbPool {
49 | mut:
50 | pool &pool.ConnectionPool
51 | backend string
52 | }
53 |
54 | // Factory for PostgreSQL pool
55 | pub fn new_pg_pool(config pg.Config, pool_cfg pool.ConnectionPoolConfig) !DbPool {
56 | factory := fn [config] () !&pool.ConnectionPoolable {
57 | mut db := pg.connect(config)!
58 | return &db
59 | }
60 | mut p := pool.new_connection_pool(factory, pool_cfg)!
61 | return DbPool{
62 | pool: p
63 | backend: 'pg'
64 | }
65 | }
66 |
67 | // Factory for SQLite pool
68 | pub fn new_sqlite_pool(path string, pool_cfg pool.ConnectionPoolConfig) !DbPool {
69 | factory := fn [path] () !&pool.ConnectionPoolable {
70 | mut db := sqlite.connect(path)!
71 | return &db
72 | }
73 | mut p := pool.new_connection_pool(factory, pool_cfg)!
74 | return DbPool{
75 | pool: p
76 | backend: 'sqlite'
77 | }
78 | }
79 |
80 | // Acquire a DB connection from the pool
81 | pub fn (mut p DbPool) acquire() !DbConn {
82 | mut conn := p.pool.get()!
83 | if p.backend == 'pg' {
84 | return conn as pg.DB
85 | } else {
86 | return conn as sqlite.DB
87 | }
88 | }
89 |
90 | // Return a DB connection to the pool
91 | pub fn (mut p DbPool) release(conn DbConn) ! {
92 | p.pool.put(conn)!
93 | }
94 |
95 | pub fn (mut p DbPool) close() ! {
96 | p.pool.close()
97 | }
98 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/infrastructure/repositories/sqlite_user_repository.v:
--------------------------------------------------------------------------------
1 | module repositories
2 |
3 | import domain
4 | import db.sqlite
5 | import rand
6 |
7 | pub struct SqliteUserRepository {
8 | get_conn fn () !sqlite.DB @[required]
9 | release_conn fn (sqlite.DB) ! @[required]
10 | }
11 |
12 | pub fn new_sqlite_user_repository(get_conn fn () !sqlite.DB, release_conn fn (sqlite.DB) !) SqliteUserRepository {
13 | return SqliteUserRepository{
14 | get_conn: get_conn
15 | release_conn: release_conn
16 | }
17 | }
18 |
19 | pub fn (r SqliteUserRepository) find_by_id(id string) !domain.User {
20 | mut db := r.get_conn()!
21 | defer { r.release_conn(db) or { panic(err) } }
22 | rows := db.exec_param_many('SELECT id, username, email, password FROM users WHERE id = ?',
23 | [id])!
24 | if rows.len == 0 {
25 | return error('not found')
26 | }
27 | row := rows[0]
28 | return domain.User{
29 | id: row.vals[0]
30 | username: row.vals[1]
31 | email: row.vals[2]
32 | password: row.vals[3]
33 | }
34 | }
35 |
36 | pub fn (r SqliteUserRepository) find_by_username(username string) !domain.User {
37 | mut db := r.get_conn()!
38 | defer { r.release_conn(db) or { panic(err) } }
39 | rows := db.exec_param_many('SELECT id, username, email, password FROM users WHERE username = ?',
40 | [username])!
41 | if rows.len == 0 {
42 | return error('not found')
43 | }
44 | row := rows[0]
45 | return domain.User{
46 | id: row.vals[0]
47 | username: row.vals[1]
48 | email: row.vals[2]
49 | password: row.vals[3]
50 | }
51 | }
52 |
53 | pub fn (r SqliteUserRepository) create(user domain.User) !domain.User {
54 | mut db := r.get_conn()!
55 | defer { r.release_conn(db) or { panic(err) } }
56 | id := if user.id == '' { rand.uuid_v4() } else { user.id }
57 | db.exec_param_many('INSERT INTO users (id, username, email, password) VALUES (?, ?, ?, ?)',
58 | [id, user.username, user.email, user.password])!
59 | return domain.User{
60 | id: id
61 | username: user.username
62 | email: user.email
63 | password: user.password
64 | }
65 | }
66 |
67 | pub fn (r SqliteUserRepository) list() ![]domain.User {
68 | mut db := r.get_conn()!
69 | defer { r.release_conn(db) or { panic(err) } }
70 | mut users := []domain.User{}
71 | rows := db.exec('SELECT id, username, email, password FROM users')!
72 | for row in rows {
73 | users << domain.User{
74 | id: row.vals[0]
75 | username: row.vals[1]
76 | email: row.vals[2]
77 | password: row.vals[3]
78 | }
79 | }
80 | return users
81 | }
82 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/infrastructure/repositories/pg_user_repository.v:
--------------------------------------------------------------------------------
1 | module repositories
2 |
3 | import domain
4 | import db.pg
5 | import rand
6 |
7 | pub struct PgUserRepository {
8 | get_conn fn () !pg.DB @[required]
9 | release_conn fn (pg.DB) ! @[required]
10 | }
11 |
12 | pub fn new_pg_user_repository(get_conn fn () !pg.DB, release_conn fn (pg.DB) !) PgUserRepository {
13 | return PgUserRepository{
14 | get_conn: get_conn
15 | release_conn: release_conn
16 | }
17 | }
18 |
19 | pub fn (r PgUserRepository) find_by_id(id string) !domain.User {
20 | mut db := r.get_conn()!
21 | defer { r.release_conn(db) or { panic(err) } }
22 | rows := db.exec_param_many('SELECT id, username, email, password FROM users WHERE id = $1',
23 | [id])!
24 | if rows.len == 0 {
25 | return error('not found')
26 | }
27 | row := rows[0]
28 | return domain.User{
29 | id: row.vals[0] or { '' }
30 | username: row.vals[1] or { '' }
31 | email: row.vals[2] or { '' }
32 | password: row.vals[3] or { '' }
33 | }
34 | }
35 |
36 | pub fn (r PgUserRepository) find_by_username(username string) !domain.User {
37 | mut db := r.get_conn()!
38 | defer { r.release_conn(db) or { panic(err) } }
39 | rows := db.exec_param_many('SELECT id, username, email, password FROM users WHERE username = $1',
40 | [username])!
41 | if rows.len == 0 {
42 | return error('not found')
43 | }
44 | row := rows[0]
45 | return domain.User{
46 | id: row.vals[0] or { '' }
47 | username: row.vals[1] or { '' }
48 | email: row.vals[2] or { '' }
49 | password: row.vals[3] or { '' }
50 | }
51 | }
52 |
53 | pub fn (r PgUserRepository) create(user domain.User) !domain.User {
54 | mut db := r.get_conn()!
55 | defer { r.release_conn(db) or { panic(err) } }
56 | id := if user.id == '' { rand.uuid_v4() } else { user.id }
57 | db.exec_param_many('INSERT INTO users (id, username, email, password) VALUES ($1, $2, $3, $4)',
58 | [id, user.username, user.email, user.password])!
59 | return domain.User{
60 | id: id
61 | username: user.username
62 | email: user.email
63 | password: user.password
64 | }
65 | }
66 |
67 | pub fn (r PgUserRepository) list() ![]domain.User {
68 | mut db := r.get_conn()!
69 | defer { r.release_conn(db) or { panic(err) } }
70 | mut users := []domain.User{}
71 | rows := db.exec_param_many('SELECT id, username, email, password FROM users', [])!
72 | for row in rows {
73 | users << domain.User{
74 | id: row.vals[0] or { '' }
75 | username: row.vals[1] or { '' }
76 | email: row.vals[2] or { '' }
77 | password: row.vals[3] or { '' }
78 | }
79 | }
80 | return users
81 | }
82 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/infrastructure/http/controllers.v:
--------------------------------------------------------------------------------
1 | module http
2 |
3 | import strings
4 | import application
5 | import json
6 |
7 | const http_ok = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n'.bytes()
8 | const http_created = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\n'.bytes()
9 |
10 | const http_not_modified = 'HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
11 | const http_bad_request = 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
12 | const http_not_found = 'HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
13 | const http_server_error = 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
14 |
15 | const content_length_header = 'Content-Length: '.bytes()
16 | const connection_close_header = 'Connection: close\r\n\r\n'.bytes()
17 |
18 | // Helper to build HTTP response
19 | fn build_response(header []u8, body string) ![]u8 {
20 | mut sb := strings.new_builder(200)
21 | sb.write(header)!
22 | sb.write(content_length_header)!
23 | sb.write_string(body.len.str())
24 | sb.write_u8(u8(`\r`))
25 | sb.write_u8(u8(`\n`))
26 | sb.write_string(body)
27 | return sb
28 | }
29 |
30 | // User registration handler
31 | pub fn handle_register(user_uc application.UserUseCase, username string, email string, password string) []u8 {
32 | user := user_uc.register(username, email, password) or { return http_bad_request }
33 | body := json.encode(user)
34 | return build_response(http_created, body) or { http_server_error }
35 | }
36 |
37 | // User list handler
38 | pub fn handle_list_users(user_uc application.UserUseCase) []u8 {
39 | users := user_uc.list_users() or { return http_server_error }
40 | body := json.encode(users)
41 | return build_response(http_ok, body) or { http_server_error }
42 | }
43 |
44 | // Product add handler
45 | pub fn handle_add_product(product_uc application.ProductUseCase, name string, price f64) []u8 {
46 | product := product_uc.add_product(name, price) or { return http_bad_request }
47 | body := json.encode(product)
48 | return build_response(http_created, body) or { http_server_error }
49 | }
50 |
51 | // Product list handler
52 | pub fn handle_list_products(product_uc application.ProductUseCase) []u8 {
53 | products := product_uc.list_products() or { return http_server_error }
54 | body := json.encode(products)
55 | return build_response(http_ok, body) or { http_server_error }
56 | }
57 |
58 | // Login handler
59 | pub fn handle_login(auth_uc application.AuthUseCase, username string, password string) []u8 {
60 | user := auth_uc.login(username, password) or { return http_not_found }
61 |
62 | body := json.encode(user)
63 | return build_response(http_ok, body) or { http_server_error }
64 | }
65 |
--------------------------------------------------------------------------------
/examples/database/src/controllers.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import strings
4 | import http_server.http1_1.response
5 |
6 | const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
7 |
8 | const http_created_response = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
9 |
10 | const tiny_internal_server_error_response = 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
11 |
12 | fn home_controller(params []string) ![]u8 {
13 | return http_ok_response
14 | }
15 |
16 | fn get_users_controller(params []string, mut pool ConnectionPool) ![]u8 {
17 | mut db := pool.acquire() or { return tiny_internal_server_error_response }
18 | defer { pool.release(db) }
19 | rows := db.exec('SELECT * FROM users') or { return tiny_internal_server_error_response }
20 |
21 | mut response_body := strings.new_builder(200)
22 | for row in rows {
23 | response_body.write_string(row.str())
24 | response_body.write_string('\n')
25 | }
26 |
27 | // response_body_str := response_body.str()
28 | defer {
29 | unsafe {
30 | response_body.free()
31 | params.free()
32 | }
33 | }
34 |
35 | mut sb := strings.new_builder(200)
36 | sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ')
37 | sb.write_string(response_body.len.str())
38 | sb.write_string('\r\nConnection: close\r\n\r\n')
39 | sb.write(response_body)!
40 |
41 | return sb
42 | }
43 |
44 | @[direct_array_access; manualfree]
45 | fn get_user_controller(params []string, mut pool ConnectionPool) ![]u8 {
46 | if params.len == 0 {
47 | return response.tiny_bad_request_response
48 | }
49 | id := params[0]
50 | mut db := pool.acquire() or { return tiny_internal_server_error_response }
51 | defer { pool.release(db) }
52 | result := db.exec('SELECT * FROM users WHERE id = ${id}') or {
53 | return tiny_internal_server_error_response
54 | }
55 | response_body := result.map(it.str()).join('\n')
56 |
57 | mut sb := strings.new_builder(200)
58 | sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ')
59 | sb.write_string(response_body.len.str())
60 | sb.write_string('\r\nConnection: close\r\n\r\n')
61 | sb.write_string(response_body)
62 |
63 | defer {
64 | unsafe {
65 | response_body.free()
66 | params.free()
67 | }
68 | }
69 | return sb
70 | }
71 |
72 | fn create_user_controller(params []string, mut pool ConnectionPool) ![]u8 {
73 | dump('create_user_controller')
74 | mut db := pool.acquire() or { return tiny_internal_server_error_response }
75 | defer { pool.release(db) }
76 | db.exec("INSERT INTO users (name) VALUES ('new_user')") or {
77 | return tiny_internal_server_error_response
78 | }
79 | return http_created_response
80 | }
81 |
--------------------------------------------------------------------------------
/examples/hexagonal/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import db.pg
4 | import db.sqlite
5 | import domain
6 | import infrastructure.database
7 | import infrastructure.repositories
8 | import infrastructure.http
9 | import application
10 | import pool
11 | import time
12 |
13 | fn main() {
14 | // Choose database backend: "pg" or "sqlite"
15 | db_backend := 'sqlite' // change to 'pg' for PostgreSQL
16 |
17 | // Pool config
18 | pool_cfg := pool.ConnectionPoolConfig{
19 | max_conns: 10
20 | min_idle_conns: 2
21 | max_lifetime: 1 * time.hour
22 | idle_timeout: 10 * time.minute
23 | get_timeout: 5 * time.second
24 | }
25 |
26 | // User repository (switchable)
27 | user_repo := match db_backend {
28 | 'pg' {
29 | config := pg.Config{
30 | host: 'localhost'
31 | port: 5432
32 | user: 'postgres'
33 | password: 'postgres'
34 | dbname: 'hexagonal'
35 | }
36 | mut dbpool := database.new_pg_pool(config, pool_cfg) or {
37 | panic('Failed to create PG pool: ' + err.msg())
38 | }
39 | defer { dbpool.close() or { panic('Failed to close PG pool: ' + err.msg()) } }
40 | get_conn := fn [mut dbpool] () !pg.DB {
41 | conn := dbpool.acquire()!
42 | return conn as pg.DB
43 | }
44 | release_conn := fn [mut dbpool] (conn pg.DB) ! {
45 | dbpool.release(conn)!
46 | }
47 | domain.UserRepository(repositories.new_pg_user_repository(get_conn, release_conn))
48 | }
49 | 'sqlite' {
50 | mut dbpool := database.new_sqlite_pool('hexagonal.db', pool_cfg) or {
51 | panic('Failed to create SQLite pool: ' + err.msg())
52 | }
53 | defer { dbpool.close() or { panic('Failed to close SQLite pool: ' + err.msg()) } }
54 | get_conn := fn [mut dbpool] () !sqlite.DB {
55 | conn := dbpool.acquire()!
56 | return conn as sqlite.DB
57 | }
58 | release_conn := fn [mut dbpool] (conn sqlite.DB) ! {
59 | dbpool.release(conn)!
60 | }
61 | domain.UserRepository(repositories.new_sqlite_user_repository(get_conn, release_conn))
62 | }
63 | else {
64 | panic('Unknown db_backend')
65 | }
66 | }
67 |
68 | product_repo := repositories.DummyProductRepository{}
69 |
70 | // Infrastructure: auth service
71 | auth_service := http.new_simple_auth_service(user_repo)
72 |
73 | // Application: use cases
74 | user_uc := application.new_user_usecase(user_repo)
75 | product_uc := application.new_product_usecase(product_repo)
76 | auth_uc := application.new_auth_usecase(auth_service)
77 |
78 | // Example usage (replace with real HTTP server integration)
79 | println('Register user:')
80 | resp := http.handle_register(user_uc, 'alice', 'alice@example.com', 'password123')
81 | println(resp.bytestr())
82 |
83 | println('Login:')
84 | resp2 := http.handle_login(auth_uc, 'alice', 'password123')
85 | println(resp2.bytestr())
86 |
87 | println('List users:')
88 | resp3 := http.handle_list_users(user_uc)
89 | println(resp3.bytestr())
90 |
91 | println('Add product:')
92 | resp4 := http.handle_add_product(product_uc, 'Laptop', 999.99)
93 | println(resp4.bytestr())
94 | }
95 |
--------------------------------------------------------------------------------
/http_server/kqueue/kqueue_darwin.c.v:
--------------------------------------------------------------------------------
1 | // Darwin (macOS) implementation for kqueue-based HTTP server
2 |
3 | module kqueue
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | fn C.kqueue() int
12 | fn C.kevent(kq int, changelist &C.kevent, nchanges int, eventlist &C.kevent, nevents int, timeout &C.timespec) int
13 | fn C.close(fd int) int
14 | fn C.perror(s &char)
15 |
16 | // Proper constants
17 | pub const evfilt_read = i16(-1)
18 | pub const evfilt_write = i16(-2)
19 | pub const ev_add = u16(0x0001)
20 | pub const ev_delete = u16(0x0002)
21 | pub const ev_eof = u16(0x0010)
22 |
23 | // V struct for kevent (mirrors C struct)
24 | pub struct C.kevent {
25 | pub mut:
26 | ident usize
27 | filter i16
28 | flags u16
29 | fflags u32
30 | data i64
31 | udata voidptr
32 | }
33 |
34 | // Callbacks for kqueue-driven IO events
35 | pub struct KqueueEventCallbacks {
36 | pub:
37 | on_read fn (fd int) @[required]
38 | on_write fn (fd int) @[required]
39 | }
40 |
41 | // Create a new kqueue instance. Returns fd or <0 on error.
42 | pub fn create_kqueue_fd() int {
43 | kq := C.kqueue()
44 | if kq < 0 {
45 | C.perror(c'kqueue')
46 | }
47 | return kq
48 | }
49 |
50 | // Add a file descriptor to a kqueue instance with given filter (EVFILT_READ/EVFILT_WRITE).
51 | pub fn add_fd_to_kqueue(kq int, fd int, filter i16) int {
52 | mut kev := C.kevent{
53 | ident: usize(fd)
54 | filter: filter
55 | flags: ev_add
56 | fflags: 0
57 | data: 0
58 | udata: unsafe { nil }
59 | }
60 | if C.kevent(kq, &kev, 1, C.NULL, 0, C.NULL) == -1 {
61 | C.perror(c'kevent add')
62 | return -1
63 | }
64 | return 0
65 | }
66 |
67 | // Remove a file descriptor from a kqueue instance.
68 | pub fn remove_fd_from_kqueue(kq int, fd int) {
69 | mut kev := C.kevent{
70 | ident: usize(fd)
71 | flags: ev_delete
72 | }
73 | // Remove both read and write filters
74 | kev.filter = evfilt_read
75 | C.kevent(kq, &kev, 1, C.NULL, 0, C.NULL)
76 | kev.filter = evfilt_write
77 | C.kevent(kq, &kev, 1, C.NULL, 0, C.NULL)
78 | C.close(fd)
79 | }
80 |
81 | // Wait for kqueue events (used by accept loop and workers)
82 | pub fn wait_kqueue(kq int, events &C.kevent, nevents int, timeout int) int {
83 | mut ts := C.timespec{}
84 | mut tsp := &ts
85 | if timeout < 0 {
86 | tsp = C.NULL
87 | } else {
88 | tsp.tv_sec = i64(timeout / 1000)
89 | tsp.tv_nsec = i64((timeout % 1000) * 1000000)
90 | }
91 | return C.kevent(kq, C.NULL, 0, events, nevents, tsp)
92 | }
93 |
94 | // Worker event loop for kqueue io_multiplexing. Processes events for a given kqueue fd using provided callbacks.
95 | pub fn process_kqueue_events(callbacks KqueueEventCallbacks, kq int) {
96 | mut events := [1024]C.kevent{}
97 | for {
98 | nev := wait_kqueue(kq, &events[0], 1024, -1)
99 | if nev < 0 {
100 | if C.errno == C.EINTR {
101 | continue
102 | }
103 | C.perror(c'kevent wait')
104 | break
105 | }
106 | for i in 0 .. nev {
107 | fd := int(events[i].ident)
108 | if (events[i].flags & ev_eof) != 0 || events[i].fflags != 0 {
109 | remove_fd_from_kqueue(kq, fd)
110 | continue
111 | }
112 | if events[i].filter == evfilt_read {
113 | callbacks.on_read(fd)
114 | } else if events[i].filter == evfilt_write {
115 | callbacks.on_write(fd)
116 | }
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/http_server/http_server_darwin.c.v:
--------------------------------------------------------------------------------
1 | // Darwin (macOS)-specific HTTP server implementation using kqueue
2 |
3 | module http_server
4 |
5 | import kqueue
6 | import socket
7 | import http1_1.response
8 | import http1_1.request
9 |
10 | // Backend selection
11 | pub enum IOBackend {
12 | kqueue = 0 // Darwin/macOS only
13 | }
14 |
15 | fn C.perror(s &char)
16 | fn C.close(fd int) int
17 |
18 | // Handle readable client connection
19 | fn handle_readable_fd(handler fn ([]u8, int) ![]u8, kq_fd int, client_fd int) {
20 | request_buffer := request.read_request(client_fd) or {
21 | response.send_status_444_response(client_fd)
22 | kqueue.remove_fd_from_kqueue(kq_fd, client_fd)
23 | return
24 | }
25 | defer { unsafe { request_buffer.free() } }
26 |
27 | response_buffer := handler(request_buffer, client_fd) or {
28 | response.send_bad_request_response(client_fd)
29 | kqueue.remove_fd_from_kqueue(kq_fd, client_fd)
30 | return
31 | }
32 |
33 | response.send_response(client_fd, response_buffer.data, response_buffer.len) or {
34 | kqueue.remove_fd_from_kqueue(kq_fd, client_fd)
35 | return
36 | }
37 |
38 | // Close connection (no keep-alive for simplicity)
39 | kqueue.remove_fd_from_kqueue(kq_fd, client_fd)
40 | }
41 |
42 | // Accept loop for main thread
43 | fn handle_accept_loop(socket_fd int, main_kq int, worker_kqs []int) {
44 | mut worker_idx := 0
45 | mut events := [1]C.kevent{}
46 |
47 | for {
48 | nev := kqueue.wait_kqueue(main_kq, &events[0], 1, -1)
49 | if nev <= 0 {
50 | if nev < 0 && C.errno != C.EINTR {
51 | C.perror(c'kevent accept')
52 | }
53 | continue
54 | }
55 |
56 | if events[0].filter == kqueue.evfilt_read {
57 | for {
58 | client_fd := C.accept(socket_fd, C.NULL, C.NULL)
59 | if client_fd < 0 {
60 | break
61 | }
62 | socket.set_blocking(client_fd, false)
63 |
64 | target_kq := worker_kqs[worker_idx]
65 | worker_idx = (worker_idx + 1) % worker_kqs.len
66 |
67 | if kqueue.add_fd_to_kqueue(target_kq, client_fd, kqueue.evfilt_read) < 0 {
68 | C.close(client_fd)
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 | pub fn run_kqueue_backend(socket_fd int, handler fn ([]u8, int) ![]u8, port int, mut threads []thread) {
76 | main_kq := kqueue.create_kqueue_fd()
77 | if main_kq < 0 {
78 | return
79 | }
80 | if kqueue.add_fd_to_kqueue(main_kq, socket_fd, kqueue.evfilt_read) < 0 {
81 | C.close(main_kq)
82 | return
83 | }
84 |
85 | n_workers := max_thread_pool_size
86 | mut worker_kqs := []int{len: n_workers}
87 |
88 | for i in 0 .. n_workers {
89 | kq := kqueue.create_kqueue_fd()
90 | if kq < 0 {
91 | // Cleanup already created
92 | for j in 0 .. i {
93 | C.close(worker_kqs[j])
94 | }
95 | C.close(main_kq)
96 | return
97 | }
98 | worker_kqs[i] = kq
99 |
100 | callbacks := kqueue.KqueueEventCallbacks{
101 | on_read: fn [handler, kq] (fd int) {
102 | handle_readable_fd(handler, kq, fd)
103 | }
104 | on_write: fn (_ int) {}
105 | }
106 | threads[i] = spawn kqueue.process_kqueue_events(callbacks, kq)
107 | }
108 |
109 | println('listening on http://localhost:${port}/ (kqueue)')
110 | handle_accept_loop(socket_fd, main_kq, worker_kqs)
111 | }
112 |
113 | pub fn (mut server Server) run() {
114 | match server.io_multiplexing {
115 | .kqueue {
116 | run_kqueue_backend(server.socket_fd, server.request_handler, server.port, mut
117 | server.threads)
118 | }
119 | else {
120 | eprintln('Only kqueue is supported on macOS/Darwin.')
121 | exit(1)
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/examples/sse/src/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import http_server
4 | import http_server.http1_1.response
5 | import http_server.http1_1.request_parser
6 | import time
7 | import sync
8 |
9 | // Shared state to manage connected SSE clients
10 | struct ClientManager {
11 | mut:
12 | clients map[int]int
13 | mutex &sync.Mutex = sync.new_mutex()
14 | }
15 |
16 | fn (mut manager ClientManager) add_client(client_conn_fd int) {
17 | manager.mutex.lock()
18 | manager.clients[client_conn_fd] = client_conn_fd
19 | manager.mutex.unlock()
20 | }
21 |
22 | fn (mut manager ClientManager) remove_client(client_conn_fd int) {
23 | manager.mutex.lock()
24 | manager.clients.delete(client_conn_fd)
25 | manager.mutex.unlock()
26 | }
27 |
28 | fn (mut manager ClientManager) get_clients() []int {
29 | manager.mutex.lock()
30 | clients := manager.clients.keys()
31 | manager.mutex.unlock()
32 | return clients
33 | }
34 |
35 | fn sse_handler(client_conn_fd int, mut manager ClientManager) {
36 | println('New SSE client connected: ${client_conn_fd}')
37 |
38 | manager.add_client(client_conn_fd)
39 |
40 | headers := 'HTTP/1.1 200 OK\r\n' + 'Content-Type: text/event-stream\r\n' +
41 | 'Cache-Control: no-cache\r\n' + 'Connection: keep-alive\r\n' +
42 | 'Access-Control-Allow-Origin: *\r\n\r\n'
43 | C.send(client_conn_fd, headers.str, headers.len, 0)
44 |
45 | // Keep the connection alive and listen for events
46 | for {
47 | time.sleep(1 * time.second) // Keep the thread alive
48 | // Client disconnection is handled by the server (EPOLLHUP/EPOLLERR)
49 | }
50 | }
51 |
52 | // send_notification Broadcasts a message to all connected clients
53 | fn send_notification(mut manager ClientManager, message string) {
54 | println('Sending notification to all clients: ${message}')
55 | clients := manager.get_clients()
56 | for client_conn_fd in clients {
57 | println('Sending to client: ${client_conn_fd}')
58 | event := 'data: ${message}\n\n'
59 | sent := C.send(client_conn_fd, event.str, event.len, 0)
60 | if sent < 0 {
61 | // Remove disconnected clients
62 | manager.remove_client(client_conn_fd)
63 | }
64 | }
65 | }
66 |
67 | fn handle_request(req_buffer []u8, client_conn_fd int, mut manager ClientManager) ![]u8 {
68 | req := request_parser.decode_http_request(req_buffer)!
69 | method := unsafe { tos(&req.buffer[req.method.start], req.method.len) }
70 | path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }
71 |
72 | match method {
73 | 'GET' {
74 | if path == '/sse' {
75 | // Spawn a new thread to handle the SSE connection
76 | spawn sse_handler(client_conn_fd, mut manager)
77 | // Return an empty response to keep the connection open
78 | return []u8{}
79 | }
80 | }
81 | 'POST' {
82 | if path == '/notification' {
83 | notification := 'Notification at ${time.utc().format_ss()}'
84 | spawn send_notification(mut manager, notification)
85 | return 'HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
86 | }
87 | }
88 | else {}
89 | }
90 |
91 | return response.tiny_bad_request_response
92 | }
93 |
94 | fn main() {
95 | mut manager := ClientManager{}
96 |
97 | mut server := http_server.new_server(http_server.ServerConfig{
98 | port: 3000
99 | io_multiplexing: unsafe { http_server.IOBackend(0) }
100 | request_handler: fn [mut manager] (req_buffer []u8, client_conn_fd int) ![]u8 {
101 | return handle_request(req_buffer, client_conn_fd, mut manager)
102 | }
103 | })!
104 | println('Server running on http://localhost:3001')
105 | server.run()
106 | }
107 |
--------------------------------------------------------------------------------
/examples/hexagonal/HEXAGONAL_ARCHITETURE.md:
--------------------------------------------------------------------------------
1 | # Hexagonal Architecture in This Project
2 |
3 | This project follows the Hexagonal Architecture (also known as Ports and Adapters) to ensure a clean separation between business logic and external systems such as databases and web frameworks.
4 |
5 | ## Key Concepts
6 |
7 | - **Domain Layer**: Contains core business entities and repository interfaces. It is independent of any external technology.
8 | - **Application Layer**: Implements use cases and orchestrates business logic using domain interfaces.
9 | - **Infrastructure Layer**: Provides concrete implementations for external systems (e.g., databases, HTTP servers) and implements the interfaces defined in the domain layer.
10 | - **Main (Composition Root)**: Wires together the application by injecting infrastructure implementations into the application and domain layers.
11 |
12 | ## Directory Structure
13 |
14 | ```
15 | examples/hexagonal/
16 | src/
17 | domain/ # Business entities and repository interfaces
18 | application/ # Use cases
19 | infrastructure/ # Adapters for DB, HTTP, etc.
20 | database/ # DB connection and pooling
21 | http/ # HTTP server and middleware
22 | repositories/ # DB repository implementations
23 | main.v # Composition root
24 | ```
25 |
26 | ## How It Works
27 |
28 | - The **domain** layer defines interfaces (ports) such as `UserRepository`, `ProductRepository`, or `PaymentService`. These describe what your business logic needs, not how it is implemented.
29 | - The **infrastructure** layer provides adapters for external systems (e.g., databases, payment providers like Stripe or PayPal, HTTP servers). These adapters implement the interfaces defined in the domain layer.
30 | - The **application** layer uses only the interfaces, remaining agnostic to the actual technology or provider.
31 | - The **main** function wires everything together, choosing which infrastructure implementation to use and injecting it into the application.
32 |
33 | ## Why External Integrations Belong in Infrastructure
34 |
35 | External systems (databases, payment gateways, messaging services, etc.) are subject to change and are not part of your core business logic. By placing their adapters in the infrastructure layer:
36 |
37 | - **Separation of concerns**: Your domain and application layers remain focused on business rules, not technology details.
38 | - **Testability**: You can test your business logic with mock implementations, without needing real external systems.
39 | - **Flexibility**: You can swap out or add new providers (e.g., switch from Stripe to PayPal) by changing only the infrastructure layer.
40 | - **Maintainability**: Changes to external APIs or libraries are isolated from your core logic.
41 |
42 | ## Extending with Payment Providers (Example)
43 |
44 | Suppose you want to support payments via Stripe and PayPal:
45 |
46 | 1. **Define a PaymentService interface in the domain layer**
47 |
48 | ```v
49 | // domain/payment.v
50 | pub interface PaymentService {
51 | charge(amount_in_cents int, currency string, source string) !PaymentResult
52 | refund(payment_id string) !bool
53 | }
54 | ```
55 |
56 | 2. **Implement adapters in the infrastructure layer**
57 |
58 | - `infrastructure/payments/stripe_payment_service.v` implements `PaymentService` for Stripe.
59 | - `infrastructure/payments/paypal_payment_service.v` implements `PaymentService` for PayPal.
60 |
61 | 3. **Inject the desired implementation in main**
62 |
63 | - The main function wires up the correct payment adapter and injects it into the application layer.
64 |
65 | ## Example
66 |
67 | - `domain/user.v` defines the `UserRepository` interface.
68 | - `infrastructure/repositories/pg_user_repository.v` implements `UserRepository` for PostgreSQL.
69 | - `main.v` selects and injects the desired repository implementation.
70 | - `domain/payment.v` defines the `PaymentService` interface.
71 | - `infrastructure/payments/stripe_payment_service.v` implements Stripe integration.
72 |
73 | ---
74 |
--------------------------------------------------------------------------------
/http_server/socket/socket_windows.c.v:
--------------------------------------------------------------------------------
1 | module socket
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | // Windows-specific socket helpers
8 |
9 | pub fn init_winsock() ! {
10 | mut wsa_data := WSAData{}
11 | if C.WSAStartup(0x202, &wsa_data) != 0 {
12 | return error('WSAStartup failed')
13 | }
14 | }
15 |
16 | pub fn cleanup_winsock() {
17 | C.WSACleanup()
18 | }
19 |
20 | type SOCKET = u64
21 |
22 | const invalid_socket = u64(~u64(0))
23 | const socket_error = -1
24 |
25 | fn C.WSAStartup(wVersionRequired u16, lpWSAData voidptr) int
26 | fn C.WSACleanup() int
27 | fn C.WSAGetLastError() int
28 | fn C.closesocket(s u64) int
29 | fn C.ioctlsocket(s u64, cmd int, argptr &u32) int
30 | fn C.WSAIoctl(s u64, dwIoControlCode u32, lpvInBuffer voidptr, cbInBuffer u32,
31 | lpvOutBuffer voidptr, cbOutBuffer u32, lpcbBytesReturned &u32,
32 | lpOverlapped voidptr, lpCompletionRoutine voidptr) int
33 | fn C.accept(s u64, addr voidptr, addrlen &int) u64
34 | fn C.bind(s u64, name voidptr, namelen int) int
35 | fn C.connect(s u64, name voidptr, namelen int) int
36 | fn C.listen(s u64, backlog int) int
37 | fn C.socket(socket_family int, socket_type int, protocol int) u64
38 | fn C.setsockopt(s u64, level int, optname int, optval voidptr, optlen int) int
39 | fn C.htons(hostshort u16) u16
40 | fn C.htonl(hostlong u32) u32
41 | fn C.ntohs(netshort u16) u16
42 | fn C.ntohl(netlong u32) u32
43 | fn C.getaddrinfo(nodename &char, servname &char, hints voidptr, res &&voidptr) int
44 | fn C.freeaddrinfo(res voidptr)
45 | fn C.getnameinfo(sa voidptr, salen int, host &char, hostlen int,
46 | serv &char, servlen int, flags int) int
47 | fn C.inet_pton(af int, src &char, dst voidptr) int
48 | fn C.inet_ntop(af int, src voidptr, dst &char, size int) &char
49 |
50 | // struct C.in_addr {
51 | // s_addr u32
52 | // }
53 |
54 | // struct C.sockaddr_in {
55 | // sin_family u16
56 | // sin_port u16
57 | // sin_addr C.in_addr
58 | // sin_zero [8]u8
59 | // }
60 |
61 | // Helper for client connections (for testing)
62 | pub fn connect_to_server_on_windows(port int) !int {
63 | init_winsock() or {
64 | println('[client] Failed to initialize Winsock: ${err}')
65 | return err
66 | }
67 |
68 | println('[client] Creating client socket...')
69 | client_fd := int(C.socket(C.AF_INET, C.SOCK_STREAM, 0))
70 | if client_fd == int(invalid_socket) {
71 | println('[client] Failed to create client socket')
72 | return error('Failed to create client socket')
73 | }
74 |
75 | mut addr := C.sockaddr_in{
76 | sin_family: u16(C.AF_INET)
77 | sin_port: C.htons(u16(port))
78 | sin_addr: C.in_addr{u32(0)} // 0.0.0.0
79 | sin_zero: [8]u8{}
80 | }
81 |
82 | println('[client] Connecting to server on port ${port} (0.0.0.0)...')
83 | if C.connect(u64(client_fd), voidptr(&addr), sizeof(addr)) == socket_error {
84 | println('[client] Failed to connect to server: error=${C.WSAGetLastError()}')
85 | C.closesocket(u64(client_fd))
86 | return error('Failed to connect to server')
87 | }
88 |
89 | println('[client] Connected to server, fd=${client_fd}')
90 | return client_fd
91 | }
92 |
93 | pub fn create_server_socket_on_windows(port int) int {
94 | init_winsock() or {
95 | eprintln('Failed to initialize Winsock: ${err}')
96 | exit(1)
97 | }
98 |
99 | server_fd := int(C.socket(C.AF_INET, C.SOCK_STREAM, 0))
100 | if server_fd == int(invalid_socket) {
101 | eprintln(@LOCATION + ' Socket creation failed: ${C.WSAGetLastError()}')
102 | exit(1)
103 | }
104 |
105 | set_blocking(server_fd, false)
106 |
107 | opt := 1
108 | if C.setsockopt(u64(server_fd), C.SOL_SOCKET, C.SO_REUSEADDR, &opt, sizeof(opt)) == socket_error {
109 | eprintln(@LOCATION + ' setsockopt SO_REUSEADDR failed: ${C.WSAGetLastError()}')
110 | close_socket(server_fd)
111 | exit(1)
112 | }
113 |
114 | // Bind to INADDR_ANY (0.0.0.0)
115 | println('[server] Binding to 0.0.0.0:${port}')
116 | server_addr := C.sockaddr_in{
117 | sin_family: u16(C.AF_INET)
118 | sin_port: C.htons(port)
119 | sin_addr: C.in_addr{u32(C.INADDR_ANY)}
120 | sin_zero: [8]u8{}
121 | }
122 |
123 | if C.bind(u64(server_fd), voidptr(&server_addr), sizeof(server_addr)) == socket_error {
124 | eprintln(@LOCATION + ' Bind failed: ${C.WSAGetLastError()}')
125 | close_socket(server_fd)
126 | exit(1)
127 | }
128 |
129 | if C.listen(u64(server_fd), max_connection_size) == socket_error {
130 | eprintln(@LOCATION + ' Listen failed: ${C.WSAGetLastError()}')
131 | close_socket(server_fd)
132 | exit(1)
133 | }
134 |
135 | return server_fd
136 | }
137 |
--------------------------------------------------------------------------------
/http_server/http1_1/request_parser/request_parser.v:
--------------------------------------------------------------------------------
1 | module request_parser
2 |
3 | const empty_space = u8(` `)
4 | const cr_char = u8(`\r`)
5 | const lf_char = u8(`\n`)
6 |
7 | pub struct Slice {
8 | pub:
9 | start int
10 | len int
11 | }
12 |
13 | // TODO make fields immutable
14 | pub struct HttpRequest {
15 | pub:
16 | buffer []u8
17 | pub mut:
18 | method Slice
19 | path Slice // TODO: change to request_target (rfc9112)
20 | version Slice
21 | // header_fields Slice
22 | // body Slice
23 | }
24 |
25 | fn C.memchr(s &u8, c int, n usize) &u8
26 |
27 | // libc memchr is AVX2-accelerated via glibc IFUNC
28 | @[inline]
29 | fn find_byte(buf &u8, len int, c u8) int {
30 | unsafe {
31 | p := C.memchr(buf, c, len)
32 | if p == voidptr(nil) {
33 | return -1
34 | }
35 | return int(p - buf)
36 | }
37 | }
38 |
39 | // spec: https://datatracker.ietf.org/doc/rfc9112/
40 | // request-line is the start-line for for requests
41 | // According to RFC 9112, the request line is structured as:
42 | // `request-line = method SP request-target SP HTTP-version`
43 | // where:
44 | // METHOD is the HTTP method (e.g., GET, POST)
45 | // SP is a single space character
46 | // REQUEST-TARGET is the path or resource being requested
47 | // HTTP-VERSION is the version of HTTP being used (e.g., HTTP/1.1)
48 | // CRLF is a carriage return followed by a line feed
49 | @[direct_array_access]
50 | pub fn parse_http1_request_line(mut req HttpRequest) ! {
51 | unsafe {
52 | buf := &req.buffer[0]
53 | len := req.buffer.len
54 |
55 | if len < 12 {
56 | return error('Too short')
57 | }
58 |
59 | // METHOD
60 | pos1 := find_byte(buf, len, empty_space)
61 | if pos1 <= 0 {
62 | return error('Invalid method')
63 | }
64 | req.method = Slice{0, pos1}
65 |
66 | // PATH - skip any extra spaces
67 | mut pos2 := pos1 + 1
68 | for pos2 < len && buf[pos2] == empty_space {
69 | pos2++
70 | }
71 | if pos2 >= len {
72 | return error('Missing path')
73 | }
74 |
75 | path_start := pos2
76 | space_pos := find_byte(buf + pos2, len - pos2, empty_space)
77 | cr_pos := find_byte(buf + pos2, len - pos2, cr_char)
78 |
79 | if space_pos < 0 && cr_pos < 0 {
80 | return error('Invalid request line')
81 | }
82 |
83 | // pick earliest delimiter
84 | mut path_len := 0
85 | mut delim_pos := 0
86 | if space_pos >= 0 && (cr_pos < 0 || space_pos < cr_pos) {
87 | path_len = space_pos
88 | delim_pos = pos2 + space_pos
89 | } else {
90 | path_len = cr_pos
91 | delim_pos = pos2 + cr_pos
92 | }
93 |
94 | req.path = Slice{path_start, path_len}
95 |
96 | // VERSION
97 | if buf[delim_pos] == cr_char {
98 | // No HTTP version specified
99 | req.version = Slice{delim_pos, 0}
100 | } else {
101 | version_start := delim_pos + 1
102 | cr := find_byte(buf + version_start, len - version_start, cr_char)
103 | if cr < 0 {
104 | return error('Missing CR')
105 | }
106 | req.version = Slice{version_start, cr}
107 | delim_pos = version_start + cr
108 | }
109 |
110 | // Validate CRLF
111 | if delim_pos + 1 >= len || buf[delim_pos + 1] != lf_char {
112 | return error('Invalid CRLF')
113 | }
114 | }
115 | }
116 |
117 | pub fn decode_http_request(buffer []u8) !HttpRequest {
118 | mut req := HttpRequest{
119 | buffer: buffer
120 | }
121 |
122 | parse_http1_request_line(mut req)!
123 | return req
124 | }
125 |
126 | // Helper function to convert Slice to string for debugging
127 | pub fn (slice Slice) to_string(buffer []u8) string {
128 | if slice.len <= 0 {
129 | return ''
130 | }
131 | return buffer[slice.start..slice.start + slice.len].bytestr()
132 | }
133 |
134 | @[direct_array_access]
135 | pub fn (req HttpRequest) get_header_value_slice(name string) ?Slice {
136 | mut pos := req.version.start + req.version.len + 2 // Start after request line (CRLF)
137 | if pos >= req.buffer.len {
138 | return none
139 | }
140 |
141 | for pos < req.buffer.len {
142 | if unsafe {
143 | vmemcmp(&req.buffer[pos], name.str, name.len)
144 | } == 0 {
145 | pos += name.len
146 | if req.buffer[pos] != `:` {
147 | return none
148 | }
149 | pos++
150 | for pos < req.buffer.len && (req.buffer[pos] == ` ` || req.buffer[pos] == `\t`) {
151 | pos++
152 | }
153 | if pos >= req.buffer.len {
154 | return none
155 | }
156 | mut start := pos
157 | for pos < req.buffer.len && req.buffer[pos] != `\r` {
158 | pos++
159 | }
160 | return Slice{
161 | start: start
162 | len: pos - start
163 | }
164 | }
165 | if req.buffer[pos] == `\r` {
166 | pos++
167 | if pos < req.buffer.len && req.buffer[pos] == `\n` {
168 | pos++
169 | }
170 | } else {
171 | pos++
172 | }
173 | }
174 |
175 | return none
176 | }
177 |
--------------------------------------------------------------------------------
/http_server/README.md:
--------------------------------------------------------------------------------
1 | # HTTP Server Module
2 |
3 | A high-performance, epoll-based HTTP server implementation for V.
4 |
5 | ## Architecture
6 |
7 | The module is organized into focused components:
8 |
9 | ### Core Files
10 |
11 | #### `http_server.c.v`
12 | Main orchestration and server lifecycle management.
13 |
14 | **Key Components:**
15 | - `Server` struct: Main server configuration and state
16 | - `run()`: Server initialization, thread pool setup, and accept loop
17 | - `handle_accept_loop()`: Non-blocking connection acceptance with round-robin load balancing
18 | - `process_events()`: Epoll event loop for client connections
19 | - `handle_readable_fd()`: Request reading, handler invocation, and response sending
20 |
21 | **Threading Model:**
22 | - Main thread: Handles `accept()` via dedicated epoll instance
23 | - Worker threads: One per CPU core, each with its own epoll instance for client I/O
24 | - Round-robin distribution of accepted connections across worker threads
25 |
26 | ---
27 |
28 | #### `epoll.v`
29 | Low-level epoll abstractions for Linux I/O multiplexing.
30 |
31 | **Exports:**
32 | - `EpollEventCallbacks`: Callback interface for read/write events
33 | - `on_read fn(fd int)`: Invoked when socket is readable
34 | - `on_write fn(fd int)`: Invoked when socket is writable
35 | - `create_epoll_fd() int`: Creates new epoll instance
36 | - `add_fd_to_epoll(epoll_fd int, fd int, events u32) int`: Registers fd with events
37 | - `remove_fd_from_epoll(epoll_fd int, fd int)`: Unregisters fd
38 |
39 | **Event Flags:**
40 | - `C.EPOLLIN`: Socket readable
41 | - `C.EPOLLOUT`: Socket writable
42 | - `C.EPOLLET`: Edge-triggered mode
43 | - `C.EPOLLHUP | C.EPOLLERR`: Connection errors
44 |
45 | ---
46 |
47 | #### `socket.v`
48 | Socket creation, configuration, and lifecycle management.
49 |
50 | **Exports:**
51 | - `create_server_socket(port int) int`: Creates non-blocking TCP server socket
52 | - Enables `SO_REUSEPORT` for multi-threaded accept
53 | - Binds to `INADDR_ANY`
54 | - Sets listen backlog to `max_connection_size`
55 | - `close_socket(fd int)`: Closes socket descriptor
56 | - `set_blocking(fd int, blocking bool)`: Configures socket blocking mode (internal)
57 |
58 | **Constants:**
59 | - `max_connection_size = 1024`: Listen queue size
60 |
61 | ---
62 |
63 | #### `request.v`
64 | HTTP request reading from client sockets.
65 |
66 | **Exports:**
67 | - `read_request(client_fd int) ![]u8`: Reads complete HTTP request
68 | - Returns error if recv fails or client closes connection
69 | - Handles partial reads in non-blocking mode
70 | - 140-byte buffer chunks for efficient memory usage
71 |
72 | **Error Cases:**
73 | - `recv failed`: System error during read
74 | - `client closed connection`: EOF received
75 | - `empty request`: No data read
76 |
77 | ---
78 |
79 | #### `response.v`
80 | HTTP response transmission utilities.
81 |
82 | **Exports:**
83 | - `send_response(fd int, buffer_ptr &u8, buffer_len int) !`: Sends response buffer
84 | - Uses `MSG_NOSIGNAL | MSG_ZEROCOPY` for performance
85 | - Returns error on send failure
86 | - `send_bad_request_response(fd int)`: Sends HTTP 400 response
87 | - `send_status_444_response(fd int)`: Sends HTTP 444 (No Response)
88 |
89 | **Constants:**
90 | - `tiny_bad_request_response`: Minimal 400 response bytes
91 | - `status_444_response`: Nginx-style connection close signal
92 |
93 | ---
94 |
95 | ## Usage Example
96 |
97 | ```v
98 | import http_server
99 |
100 | fn my_handler(request []u8, client_fd int) ![]u8 {
101 | // Parse request, generate response
102 | return 'HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!'.bytes()
103 | }
104 |
105 | mut server := http_server.Server{
106 | port: 8080
107 | request_handler: my_handler
108 | }
109 |
110 | server.run() // Blocks until shutdown
111 | ```
112 |
113 | ## Performance Characteristics
114 |
115 | - **Connection Handling**: O(1) epoll operations per event
116 | - **Memory**: 140-byte buffers per active read operation
117 | - **Concurrency**: N worker threads (N = CPU cores)
118 | - **Load Balancing**: Round-robin accept distribution
119 |
120 | ## Platform Support
121 |
122 | - **Linux**: Full support via epoll
123 | - **Windows**: Not supported (use WSL)
124 | - **macOS**: Not supported (epoll unavailable)
125 |
126 | ## Thread Safety
127 |
128 | - Each worker thread has isolated epoll instance
129 | - No shared mutable state between workers
130 | - Request handler must be thread-safe (receives immutable request slice)
131 |
132 | ## Error Handling
133 |
134 | Connection errors trigger automatic cleanup:
135 | 1. Remove fd from epoll
136 | 2. Close socket
137 | 3. Continue processing remaining events
138 |
139 | Request/response errors are logged but don't crash the server.
140 |
--------------------------------------------------------------------------------
/http_server/socket/socket_tcp.c.v:
--------------------------------------------------------------------------------
1 | module socket
2 |
3 | pub const max_connection_size = 1024
4 |
5 | #include
6 | #include
7 |
8 | $if !windows {
9 | #include
10 | }
11 |
12 | fn C.socket(socket_family int, socket_type int, protocol int) int
13 |
14 | $if linux {
15 | fn C.bind(sockfd int, addr &C.sockaddr_in, addrlen u32) int
16 | } $else {
17 | fn C.bind(sockfd int, addr voidptr, addrlen u32) int // Use voidptr for generic sockaddr
18 | }
19 | fn C.setsockopt(__fd int, __level int, __optname int, __optval voidptr, __optlen u32) int
20 | fn C.listen(__fd int, __n int) int
21 | fn C.perror(s &char)
22 | fn C.close(fd int) int
23 |
24 | $if linux {
25 | fn C.accept(sockfd int, address &C.sockaddr_in, addrlen &u32) int
26 | } $else {
27 | fn C.accept(sockfd int, address voidptr, addrlen &u32) int // Use voidptr here too
28 | }
29 | fn C.htons(__hostshort u16) u16
30 | fn C.fcntl(fd int, cmd int, arg int) int
31 | fn C.connect(sockfd int, addr &C.sockaddr_in, addrlen u32) int
32 |
33 | struct C.in_addr {
34 | s_addr u32
35 | }
36 |
37 | struct C.sockaddr_in {
38 | sin_family u16
39 | sin_port u16
40 | sin_addr C.in_addr
41 | sin_zero [8]u8
42 | }
43 |
44 | // Helper for client connections (for testing)
45 | pub fn connect_to_server(port int) !int {
46 | println('[client] Creating client socket...')
47 | $if windows {
48 | return connect_to_server_on_windows(port)
49 | }
50 |
51 | client_fd := C.socket(C.AF_INET, C.SOCK_STREAM, 0)
52 | if client_fd < 0 {
53 | println('[client] Failed to create client socket')
54 | return error('Failed to create client socket')
55 | }
56 | mut addr := C.sockaddr_in{
57 | sin_family: u16(C.AF_INET)
58 | sin_port: C.htons(u16(port))
59 | sin_addr: C.in_addr{u32(0)} // 0.0.0.0
60 | sin_zero: [8]u8{}
61 | }
62 | println('[client] Connecting to server on port ${port} (0.0.0.0)...')
63 | // Cast to voidptr for OS compatibility
64 | if C.connect(client_fd, voidptr(&addr), sizeof(addr)) < 0 {
65 | println('[client] Failed to connect to server')
66 | C.close(client_fd)
67 | return error('Failed to connect to server')
68 | }
69 | println('[client] Connected to server, fd=${client_fd}')
70 | return client_fd
71 | }
72 |
73 | // Setup and teardown for server sockets.
74 |
75 | pub fn set_blocking(fd int, blocking bool) {
76 | $if windows {
77 | mut mode := u32(if blocking { 0 } else { 1 })
78 | if C.ioctlsocket(u64(fd), 0x8004667E, &mode) != 0 // FIONBIO
79 | {
80 | eprintln(@LOCATION + ' ioctlsocket failed: ${C.WSAGetLastError()}')
81 | }
82 | } $else {
83 | flags := C.fcntl(fd, C.F_GETFL, 0)
84 | if flags == -1 {
85 | eprintln(@LOCATION)
86 | return
87 | }
88 | new_flags := if blocking { flags & ~C.O_NONBLOCK } else { flags | C.O_NONBLOCK }
89 | C.fcntl(fd, C.F_SETFL, new_flags)
90 | }
91 | }
92 |
93 | pub fn close_socket(fd int) {
94 | $if windows {
95 | C.closesocket(u64(fd))
96 | } $else {
97 | C.close(fd)
98 | }
99 | }
100 |
101 | pub fn create_server_socket(port int) int {
102 | $if windows {
103 | return create_server_socket_on_windows(port)
104 | }
105 | server_fd := C.socket(C.AF_INET, C.SOCK_STREAM, 0)
106 | if server_fd < 0 {
107 | eprintln(@LOCATION)
108 | C.perror(c'Socket creation failed')
109 | exit(1)
110 | }
111 |
112 | set_blocking(server_fd, false)
113 |
114 | opt := 1
115 | $if linux {
116 | // On Linux/other Unix, use SO_REUSEPORT for socket sharding/load balancing
117 | // SO_REUSEPORT allows multiple workers to bind() and accept() independently
118 | if C.setsockopt(server_fd, C.SOL_SOCKET, C.SO_REUSEPORT, &opt, sizeof(opt)) < 0 {
119 | eprintln(@LOCATION)
120 | C.perror(c'setsockopt SO_REUSEPORT failed')
121 | close_socket(server_fd)
122 | exit(1)
123 | }
124 | } $else {
125 | if C.setsockopt(server_fd, C.SOL_SOCKET, C.SO_REUSEADDR, &opt, sizeof(opt)) < 0 {
126 | eprintln(@LOCATION)
127 | C.perror(c'setsockopt SO_REUSEADDR failed')
128 | close_socket(server_fd)
129 | exit(1)
130 | }
131 | }
132 |
133 | // Bind to INADDR_ANY (0.0.0.0)
134 | println('[server] Binding to 0.0.0.0:${port}')
135 | server_addr := C.sockaddr_in{
136 | sin_family: u16(C.AF_INET)
137 | sin_port: C.htons(port)
138 | sin_addr: C.in_addr{u32(C.INADDR_ANY)}
139 | sin_zero: [8]u8{}
140 | }
141 |
142 | // Cast to voidptr to fix the type mismatch
143 | if C.bind(server_fd, voidptr(&server_addr), sizeof(server_addr)) < 0 {
144 | eprintln(@LOCATION)
145 | C.perror(c'Bind failed')
146 | close_socket(server_fd)
147 | exit(1)
148 | }
149 |
150 | if C.listen(server_fd, max_connection_size) < 0 {
151 | eprintln(@LOCATION)
152 | C.perror(c'Listen failed')
153 | close_socket(server_fd)
154 | exit(1)
155 | }
156 |
157 | return server_fd
158 | }
159 |
--------------------------------------------------------------------------------
/http_server/iocp/iocp_windows.c.v:
--------------------------------------------------------------------------------
1 | module iocp
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | @[typedef]
8 | pub struct C.OVERLAPPED {
9 | pub mut:
10 | internal u64
11 | internal_high u64
12 | offset u64
13 | offset_high u64
14 | h_event voidptr
15 | }
16 |
17 | pub struct CompletionKey {
18 | pub:
19 | socket_handle int
20 | operation IOOperation
21 | callback fn (int, IOOperation, []u8) @[required] // socket_fd, operation, data
22 | }
23 |
24 | pub enum IOOperation {
25 | accept
26 | read
27 | write
28 | close
29 | }
30 |
31 | pub struct IOData {
32 | pub mut:
33 | overlapped C.OVERLAPPED
34 | operation IOOperation
35 | socket_fd int
36 | wsabuf C.WSABUF
37 | buffer []u8
38 | bytes_transferred u32
39 | }
40 |
41 | @[typedef]
42 | pub struct C.WSABUF {
43 | pub mut:
44 | len u32
45 | buf &u8
46 | }
47 |
48 | fn C.CreateIoCompletionPort(file_handle voidptr, existing_completion_port voidptr,
49 | completion_key u64, number_of_concurrent_threads u32) voidptr
50 |
51 | fn C.GetQueuedCompletionStatus(completion_port voidptr, lp_number_of_bytes_transferred &u32,
52 | lp_completion_key &u64, lp_overlapped &&C.OVERLAPPED, dw_milliseconds u32) bool
53 |
54 | fn C.PostQueuedCompletionStatus(completion_port voidptr, dw_number_of_bytes_transferred u32,
55 | dw_completion_key u64, lp_overlapped &C.OVERLAPPED) bool
56 |
57 | fn C.CloseHandle(h_object voidptr) bool
58 |
59 | fn C.WSARecv(s u64, lp_buffers &C.WSABUF, dw_buffer_count u32, lp_number_of_bytes_recvd &u32,
60 | lp_flags &u32, lp_overlapped &C.OVERLAPPED, lp_completion_routine voidptr) int
61 |
62 | fn C.WSASend(s u64, lp_buffers &C.WSABUF, dw_buffer_count u32, lp_number_of_bytes_sent &u32,
63 | dw_flags u32, lp_overlapped &C.OVERLAPPED, lp_completion_routine voidptr) int
64 |
65 | fn C.AcceptEx(s_listen_socket u64, s_accept_socket u64, lp_output_buffer voidptr,
66 | dw_receive_data_length u32, dw_local_address_length u32, dw_remote_address_length u32,
67 | lpdw_bytes_received &u32, lp_overlapped &C.OVERLAPPED) bool
68 |
69 | fn C.GetAcceptExSockaddrs(lp_output_buffer voidptr, dw_receive_data_length u32,
70 | dw_local_address_length u32, dw_remote_address_length u32,
71 | local_sockaddr &&voidptr, local_sockaddr_length &int,
72 | remote_sockaddr &&voidptr, remote_sockaddr_length &int)
73 |
74 | fn C.CreateEventA(lp_event_attributes voidptr, b_manual_reset bool, b_initial_state bool,
75 | lp_name &u16) voidptr
76 |
77 | fn C.SetEvent(h_event voidptr) bool
78 |
79 | fn C.WaitForSingleObject(h_handle voidptr, dw_milliseconds u32) u32
80 |
81 | const infinity = 0xFFFFFFFF
82 |
83 | pub struct IOCP {
84 | pub mut:
85 | handle voidptr
86 | worker_threads []thread
87 | shutdown_signal voidptr
88 | }
89 |
90 | pub fn create_iocp(max_concurrent_threads u32) !voidptr {
91 | handle := C.CreateIoCompletionPort(unsafe { nil }, unsafe { nil }, 0, max_concurrent_threads)
92 | if handle == unsafe { nil } {
93 | return error('Failed to create IOCP port')
94 | }
95 | return handle
96 | }
97 |
98 | pub fn associate_handle_with_iocp(iocp_handle voidptr, socket_handle int, completion_key u64) ! {
99 | handle := C.CreateIoCompletionPort(voidptr(socket_handle), iocp_handle, completion_key,
100 | 0)
101 | if handle == unsafe { nil } {
102 | return error('Failed to associate socket with IOCP')
103 | }
104 | }
105 |
106 | pub fn post_iocp_status(iocp_handle voidptr, bytes_transferred u32, completion_key u64,
107 | overlapped &C.OVERLAPPED) bool {
108 | return C.PostQueuedCompletionStatus(iocp_handle, bytes_transferred, completion_key,
109 | overlapped)
110 | }
111 |
112 | pub fn get_queued_completion_status(iocp_handle voidptr, bytes_transferred &u32,
113 | completion_key &u64, overlapped &&C.OVERLAPPED, timeout_ms u32) bool {
114 | return C.GetQueuedCompletionStatus(iocp_handle, bytes_transferred, completion_key,
115 | overlapped, timeout_ms)
116 | }
117 |
118 | pub fn create_event() voidptr {
119 | return C.CreateEventA(unsafe { nil }, false, false, unsafe { nil })
120 | }
121 |
122 | pub fn wait_for_single_object(handle voidptr, timeout_ms u32) u32 {
123 | return C.WaitForSingleObject(handle, timeout_ms)
124 | }
125 |
126 | pub fn set_event(handle voidptr) bool {
127 | return C.SetEvent(handle)
128 | }
129 |
130 | pub fn close_handle(handle voidptr) bool {
131 | return C.CloseHandle(handle)
132 | }
133 |
134 | pub fn start_accept_ex(listen_socket int, accept_socket int, overlapped &C.OVERLAPPED) bool {
135 | return C.AcceptEx(u64(listen_socket), u64(accept_socket), unsafe { nil }, 0,
136 | sizeof(C.sockaddr_in) + 16, sizeof(C.sockaddr_in) + 16, unsafe { nil }, overlapped)
137 | }
138 |
139 | pub fn post_recv(socket_fd int, buffers &C.WSABUF, buffer_count u32, flags &u32,
140 | overlapped &C.OVERLAPPED) int {
141 | return C.WSARecv(u64(socket_fd), buffers, buffer_count, unsafe { nil }, flags, overlapped,
142 | unsafe { nil })
143 | }
144 |
145 | pub fn post_send(socket_fd int, buffers &C.WSABUF, buffer_count u32, flags u32,
146 | overlapped &C.OVERLAPPED) int {
147 | return C.WSASend(u64(socket_fd), buffers, buffer_count, unsafe { nil }, flags, overlapped,
148 | unsafe { nil })
149 | }
150 |
151 | pub fn create_io_data(socket_fd int, operation IOOperation, buffer_size int) &IOData {
152 | mut io_data := &IOData{
153 | socket_fd: socket_fd
154 | operation: operation
155 | buffer: []u8{len: buffer_size}
156 | }
157 | io_data.wsabuf.len = u32(buffer_size)
158 | io_data.wsabuf.buf = &io_data.buffer[0]
159 | return io_data
160 | }
161 |
162 | pub fn free_io_data(io_data &IOData) {
163 | unsafe {
164 | io_data.buffer.free()
165 | free(io_data)
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # HTTP Server
4 |
5 | ## Features
6 |
7 | - **Fast**: Multi-threaded, non-blocking I/O, lock-free, copy-free, I/O multiplexing, SO_REUSEPORT (native load balancing on Linux)
8 | - **Modular**: Easy to extend with custom controllers and handlers.
9 | - **Memory Safety**: No race conditions.
10 | - **No Magic**: Transparent and straightforward.
11 | - **E2E Testing**: Allows end-to-end testing and scripting without running the server. Pass raw requests to `handle_request()`.
12 | - **SSE Friendly**: Built-in Server-Sent Events support.
13 | - **ETag Friendly**: Conditional GETs with ETag and `If-None-Match` headers.
14 | - **Database Friendly**: Example with PostgreSQL connection pool.
15 | - **Graceful Shutdown**: Automatic shutdown after test mode or on signal (W.I.P.).
16 | - **Multiple Backends**: epoll, io_uring, kqueue (platform-dependent).
17 | - **Compliant with HTTP standards**: It follows [RFC9112](https://datatracker.ietf.org/doc/rfc9112/) and [IANA Field Name Registry](https://www.iana.org/assignments/http-fields/http-fields.xhtml)
18 |
19 | ---
20 |
21 | ## Usage Examples
22 |
23 | ### 1. Simple HTTP Server
24 |
25 | ```v
26 | import http_server
27 |
28 | fn handle_request(req_buffer []u8, client_conn_fd int) ![]u8 {
29 | // ...parse request and return response...
30 | }
31 |
32 | fn main() {
33 | mut server := http_server.new_server(http_server.ServerConfig{
34 | port: 3000
35 | request_handler: handle_request
36 | io_multiplexing: $if linux {
37 | .epoll
38 | } $else $if darwin {
39 | .kqueue
40 | } $else {
41 | .iocp
42 | }
43 | })
44 | server.run()
45 | }
46 | ```
47 |
48 | ### 2. End-to-End Testing
49 |
50 | ```v
51 | fn test_simple_without_init_the_server() {
52 | request := 'GET / HTTP/1.1\r\n\r\n'.bytes()
53 | assert handle_request(request, -1)! == http_ok_response
54 | }
55 | ```
56 |
57 | Or use the server’s test mode:
58 |
59 | ```v
60 | mut server := http_server.new_server(http_server.ServerConfig{ ... })
61 | responses := server.test([request1, request2]) or { panic(err) }
62 | ```
63 |
64 | ### 3. Server-Sent Events (SSE)
65 |
66 | **Server:**
67 |
68 | ```sh
69 | v -prod run examples/sse
70 | ```
71 |
72 | **Front-end:**
73 |
74 | ```html
75 |
81 | ```
82 |
83 | **Send notification:**
84 |
85 | ```sh
86 | curl -X POST http://localhost:3001/notification
87 | ```
88 |
89 | ### 4. ETag Support
90 |
91 | ```sh
92 | curl -v http://localhost:3001/user/1
93 | curl -v -H "If-None-Match: c4ca4238a0b923820dcc509a6f75849b" http://localhost:3001/user/1
94 | ```
95 |
96 | ### 5. Database Example (PostgreSQL)
97 |
98 | **Start database:**
99 |
100 | ```sh
101 | docker-compose -f examples/database/docker-compose.yml up -d
102 | ```
103 |
104 | **Run server:**
105 |
106 | ```sh
107 | v -prod run examples/database
108 | ```
109 |
110 | **Example handler:**
111 |
112 | ```v
113 | fn handle_request(req_buffer []u8, client_conn_fd int, mut pool ConnectionPool) ![]u8 {
114 | // Use pool.acquire() and pool.release() for DB access
115 | }
116 | ```
117 |
118 | ---
119 |
120 | ## Benchmarking
121 |
122 | - use `-d force_keep_alive` to make sure that client will not be "blocked" by having to create a new connection at each request
123 |
124 | ```sh
125 | wrk -t16 -c512 -d30s http://localhost:3001
126 | wrk -t16 -c512 -d30s -H "If-None-Match: c4ca4238a0b923820dcc509a6f75849b" http://localhost:3001/user/1
127 | ```
128 |
129 | ---
130 |
131 | ## More Examples
132 |
133 | - `examples/simple/` – Basic CRUD
134 | - `examples/etag/` – ETag and conditional requests
135 | - `examples/sse/` – Server-Sent Events
136 | - `examples/database/` – PostgreSQL integration
137 | - `examples/hexagonal/` – Hexagonal architecture
138 |
139 | ---
140 |
141 | ## Test Mode
142 |
143 | The `Server` provides a test method that accepts an array of raw HTTP requests, sends them directly to the socket, and processes each one sequentially. After receiving the response for the last request, the loop ends and the server shuts down automatically. This enables efficient end-to-end testing without running a persistent server process longer that needed.
144 |
145 | ## Installation
146 |
147 | ### From Root Directory
148 |
149 | 1. Create the required directories:
150 |
151 | ```bash
152 | mkdir -p ~/.vmodules/enghitalo/vanilla
153 | ```
154 |
155 | 2. Copy the `vanilla` directory to the target location:
156 |
157 | ```bash
158 | cp -r ./ ~/.vmodules/enghitalo/vanilla
159 | ```
160 |
161 | 3. Run the example:
162 |
163 | ```bash
164 | v -prod crun examples/simple
165 | ```
166 |
167 | This sets up the module in your `~/.vmodules` directory for use.
168 |
169 | ### From Repository
170 |
171 | Install directly from the repository:
172 |
173 | ```bash
174 | v install https://github.com/enghitalo/vanilla
175 | ```
176 |
177 | ## Benchmarking
178 |
179 | - use `-d force_keep_alive` to make sure that client will not be "blocked" by having to create a new connection at each request
180 |
181 | Run the following commands to benchmark the server:
182 |
183 | 1. Test with `curl`:
184 |
185 | ```bash
186 | curl -v http://localhost:3001
187 | ```
188 |
189 | 2. Test with `wrk`:
190 |
191 | ```bash
192 | wrk -H 'Connection: "keep-alive"' --connection 512 --threads 16 --duration 60s http://localhost:3001
193 | ```
194 |
195 | Example output:
196 |
197 | ```plaintext
198 | Running 1m test @ http://localhost:3001
199 | 16 threads and 512 connections
200 | Thread Stats Avg Stdev Max +/- Stdev
201 | Latency 1.25ms 1.46ms 35.70ms 84.67%
202 | Req/Sec 32.08k 2.47k 57.85k 71.47%
203 | 30662010 requests in 1.00m, 2.68GB read
204 | Requests/sec: 510197.97
205 | Transfer/sec: 45.74MB
206 | ```
207 |
--------------------------------------------------------------------------------
/http_server/http_server.c.v:
--------------------------------------------------------------------------------
1 | module http_server
2 |
3 | import runtime
4 | import socket
5 |
6 | const max_thread_pool_size = runtime.nr_cpus()
7 |
8 | struct Server {
9 | pub:
10 | port int = 3000
11 | io_multiplexing IOBackend = unsafe { IOBackend(0) }
12 | socket_fd int
13 | pub mut:
14 | threads []thread = []thread{len: max_thread_pool_size, cap: max_thread_pool_size}
15 | request_handler fn ([]u8, int) ![]u8 @[required]
16 | }
17 |
18 | fn C.send(__fd int, __buf voidptr, __n usize, __flags int) int
19 | fn C.recv(__fd int, __buf voidptr, __n usize, __flags int) int
20 | fn C.close(__fd int) int
21 |
22 | // Test method: send raw HTTP requests directly to the server socket, process sequentially, and shutdown after last response.
23 | pub fn (mut s Server) test(requests [][]u8) ![][]u8 {
24 | println('[test] Starting server thread...')
25 |
26 | $if windows {
27 | socket.init_winsock() or { return error('Failed to initialize Winsock: ${err}') }
28 | }
29 |
30 | // Use a channel to signal when the server is ready
31 | ready_ch := chan bool{cap: 1}
32 | mut threads := []thread{len: max_thread_pool_size, cap: max_thread_pool_size}
33 | spawn fn [mut s, mut threads, ready_ch] () {
34 | println('[test] Server backend setup...')
35 | ready_ch <- true
36 | $if linux {
37 | match s.io_multiplexing {
38 | .epoll {
39 | println('[test] Running epoll backend')
40 | run_epoll_backend(s.socket_fd, s.request_handler, s.port, mut threads)
41 | }
42 | .io_uring {
43 | println('[test] Running io_uring backend')
44 | run_io_uring_backend(s.request_handler, s.port, mut threads)
45 | }
46 | }
47 | } $else $if darwin {
48 | println('[test] Running kqueue backend')
49 | run_kqueue_backend(s.socket_fd, s.request_handler, s.port, mut threads)
50 | } $else $if windows {
51 | println('[test] Running IOCP backend')
52 | run_iocp_backend(s.socket_fd, s.request_handler, s.port, mut threads)
53 | } $else {
54 | eprintln('Unsupported OS for http_server.')
55 | exit(1)
56 | }
57 | }()
58 |
59 | // Wait for the server to signal readiness
60 | _ := <-ready_ch
61 | println('[test] Server signaled ready, connecting client...')
62 |
63 | mut responses := [][]u8{cap: requests.len}
64 | client_fd := socket.connect_to_server(s.port) or {
65 | eprintln('[test] Failed to connect to server: ${err}')
66 | return err
67 | }
68 |
69 | println('[test] Client connected, sending requests...')
70 | for i, req in requests {
71 | println('[test] Preparing to send request #${i + 1} (${req.len} bytes)')
72 | mut sent := 0
73 | for sent < req.len {
74 | println('[test] Sending bytes ${sent}..${sent + (req.len - sent)}')
75 | n := C.send(client_fd, &req[sent], req.len - sent, 0)
76 | if n <= 0 {
77 | println('[test] Failed to send request at byte ${sent}')
78 | C.close(client_fd)
79 | return error('Failed to send request')
80 | }
81 | sent += n
82 | }
83 | println('[test] Sent request #${i + 1}, now receiving response...')
84 | mut buf := []u8{len: 0, cap: 4096}
85 | mut tmp := [4096]u8{}
86 | for {
87 | println('[test] Waiting to receive response bytes...')
88 | n := C.recv(client_fd, &tmp[0], 4096, 0)
89 | println('[test] recv returned ${n}')
90 | if n <= 0 {
91 | break
92 | }
93 | buf << tmp[..n]
94 | // Try to parse Content-Length if present
95 | resp_str := buf.bytestr()
96 | if resp_str.index('\r\n\r\n') != none {
97 | header_end := resp_str.index('\r\n\r\n') or {
98 | return error('Failed to find end of headers in response')
99 | }
100 | headers := resp_str[..header_end]
101 | content_length_marker := 'Content-Length: '
102 | if headers.index(content_length_marker) != none {
103 | content_length_idx := headers.index(content_length_marker) or {
104 | return error('Failed to find Content-Length in headers')
105 | }
106 | start := content_length_idx + content_length_marker.len
107 | if headers.index_after('\r\n', start) != none {
108 | end := headers.index_after('\r\n', start) or {
109 | return error('Failed to find end of Content-Length header line')
110 | }
111 | content_length_str := headers[start..end].trim_space()
112 | content_length := content_length_str.int()
113 | total_len := header_end + 4 + content_length
114 | if buf.len >= total_len {
115 | break
116 | }
117 | } else {
118 | content_length_str := headers[start..].trim_space()
119 | content_length := content_length_str.int()
120 | total_len := header_end + 4 + content_length
121 | if buf.len >= total_len {
122 | break
123 | }
124 | }
125 | } else {
126 | // No Content-Length, just break after headers
127 | break
128 | }
129 | }
130 | }
131 | println('[test] Received response #${i + 1} (${buf.len} bytes)')
132 | responses << buf.clone()
133 | }
134 |
135 | C.close(client_fd)
136 | println('[test] Client closed, shutting down server socket...')
137 | // Shutdown server after last response
138 | socket.close_socket(s.socket_fd)
139 |
140 | $if windows {
141 | socket.cleanup_winsock()
142 | }
143 |
144 | println('[test] Test complete, returning responses')
145 | return responses
146 | }
147 |
148 | struct Certificates {
149 | pub:
150 | cert_pem []u8
151 | key_pem []u8
152 | ca_cert_pem []u8
153 | }
154 |
155 | pub struct ServerConfig {
156 | pub:
157 | port int = 3000
158 | io_multiplexing IOBackend = unsafe { IOBackend(0) }
159 | request_handler fn ([]u8, int) ![]u8 @[required]
160 | certificates Certificates
161 | }
162 |
163 | pub fn new_server(config ServerConfig) !Server {
164 | socket_fd := socket.create_server_socket(config.port)
165 |
166 | // Set default backend based on OS
167 | io_multiplexing := config.io_multiplexing
168 | $if windows {
169 | if io_multiplexing != .iocp {
170 | return error('Windows only supports IOCP backend')
171 | }
172 | } $else $if linux {
173 | if io_multiplexing != .epoll && io_multiplexing != .io_uring {
174 | return error('Linux only supports epoll and io_uring backends')
175 | }
176 | } $else $if darwin {
177 | if io_multiplexing != .kqueue {
178 | return error('macOS only supports kqueue backend')
179 | }
180 | }
181 |
182 | return Server{
183 | port: config.port
184 | io_multiplexing: config.io_multiplexing
185 | socket_fd: socket_fd
186 | request_handler: config.request_handler
187 | threads: []thread{len: max_thread_pool_size, cap: max_thread_pool_size}
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/http_server/http_server_io_uring_linux.c.v:
--------------------------------------------------------------------------------
1 | module http_server
2 |
3 | import io_uring
4 | import http1_1.response
5 | import socket
6 |
7 | #include
8 |
9 | fn C.perror(s &u8)
10 | fn C.sleep(seconds u32) u32
11 | fn C.close(fd int) int
12 |
13 | // --- io_uring CQE Handlers ---
14 | fn handle_io_uring_accept(worker &io_uring.Worker, cqe &C.io_uring_cqe) {
15 | res := cqe.res
16 | if res >= 0 {
17 | fd := res
18 | $if verbose ? {
19 | eprintln('[DEBUG] Accept: new fd=${fd}')
20 | }
21 | io_uring.tune_socket(fd)
22 | mut nc := io_uring.pool_acquire_from_ptr(worker, fd)
23 | if unsafe { nc != nil } {
24 | io_uring.prepare_recv(&worker.ring, mut *nc)
25 | } else {
26 | C.close(fd)
27 | }
28 | }
29 | // Re-arm accept if needed
30 | if (cqe.flags & u32(1 << 1)) == 0 || res < 0 {
31 | $if verbose ? {
32 | eprintln('[DEBUG] Re-arming accept (end of multishot or error)')
33 | }
34 | io_uring.prepare_accept(&worker.ring, worker.socket_fd, worker.use_multishot)
35 | }
36 | }
37 |
38 | fn handle_io_uring_read(worker &io_uring.Worker, cqe &C.io_uring_cqe, handler fn ([]u8, int) ![]u8) {
39 | res := cqe.res
40 | c_ptr := io_uring.decode_connection_ptr(C.io_uring_cqe_get_data64(cqe))
41 | if res <= 0 {
42 | $if verbose ? {
43 | eprintln('[DEBUG] Read EOF/error: ${res}')
44 | }
45 | if unsafe { c_ptr != nil } {
46 | mut conn := unsafe { &io_uring.Connection(c_ptr) }
47 | io_uring.pool_release_from_ptr(worker, mut *conn)
48 | }
49 | } else if unsafe { c_ptr != nil } {
50 | mut conn := unsafe { &io_uring.Connection(c_ptr) }
51 | conn.bytes_read = res
52 | $if verbose ? {
53 | eprintln('[DEBUG] Read ${res} bytes from fd=${conn.fd}')
54 | }
55 | request_data := unsafe { conn.buf[..conn.bytes_read] }
56 | response_data := handler(request_data, conn.fd) or {
57 | response.send_bad_request_response(conn.fd)
58 | io_uring.pool_release_from_ptr(worker, mut *conn)
59 | C.io_uring_cqe_seen(&worker.ring, cqe)
60 | C.io_uring_submit(&worker.ring)
61 | return
62 | }
63 | conn.response_buffer = response_data
64 | conn.bytes_sent = 0
65 | $if verbose ? {
66 | eprintln('[DEBUG] Preparing write of ${conn.response_buffer.len} bytes')
67 | }
68 | io_uring.prepare_send(&worker.ring, mut *conn, conn.response_buffer.data, usize(conn.response_buffer.len))
69 | }
70 | }
71 |
72 | fn handle_io_uring_write(worker &io_uring.Worker, cqe &C.io_uring_cqe) {
73 | res := cqe.res
74 | c_ptr := io_uring.decode_connection_ptr(C.io_uring_cqe_get_data64(cqe))
75 | if res >= 0 {
76 | $if verbose ? {
77 | eprintln('[DEBUG] Wrote ${res} bytes')
78 | }
79 | if unsafe { c_ptr != nil } {
80 | mut conn := unsafe { &io_uring.Connection(c_ptr) }
81 | conn.bytes_sent += res
82 | if conn.bytes_sent < conn.response_buffer.len {
83 | remaining := conn.response_buffer.len - conn.bytes_sent
84 | io_uring.prepare_send(&worker.ring, mut *conn, unsafe {
85 | &u8(u64(conn.response_buffer.data) + u64(conn.bytes_sent))
86 | }, usize(remaining))
87 | } else {
88 | $if verbose ? {
89 | eprintln('[DEBUG] Write complete, keep-alive next read')
90 | }
91 | conn.bytes_read = 0
92 | unsafe { conn.response_buffer.free() }
93 | conn.response_buffer = []u8{}
94 | io_uring.prepare_recv(&worker.ring, mut *conn)
95 | }
96 | }
97 | } else {
98 | $if verbose ? {
99 | eprintln('[DEBUG] Write error: ${res}')
100 | }
101 | if unsafe { c_ptr != nil } {
102 | mut conn := unsafe { &io_uring.Connection(c_ptr) }
103 | io_uring.pool_release_from_ptr(worker, mut *conn)
104 | }
105 | }
106 | }
107 |
108 | fn dispatch_io_uring_cqe(worker &io_uring.Worker, cqe &C.io_uring_cqe, handler fn ([]u8, int) ![]u8) {
109 | data := C.io_uring_cqe_get_data64(cqe)
110 | op := io_uring.decode_op_type(data)
111 | match op {
112 | io_uring.op_accept { handle_io_uring_accept(worker, cqe) }
113 | io_uring.op_read { handle_io_uring_read(worker, cqe, handler) }
114 | io_uring.op_write { handle_io_uring_write(worker, cqe) }
115 | else {}
116 | }
117 | }
118 |
119 | fn io_uring_worker_loop(worker &io_uring.Worker, handler fn ([]u8, int) ![]u8) {
120 | io_uring.prepare_accept(&worker.ring, worker.socket_fd, worker.use_multishot)
121 | C.io_uring_submit(&worker.ring)
122 | $if verbose ? {
123 | eprintln('[DEBUG] Worker started, listening on fd=${worker.socket_fd}')
124 | }
125 |
126 | for {
127 | $if verbose ? {
128 | eprintln('[DEBUG] Waiting for CQE...')
129 | }
130 | mut cqe := &C.io_uring_cqe(unsafe { nil })
131 | ret := C.io_uring_wait_cqe(&worker.ring, &cqe)
132 | if ret == -C.EINTR {
133 | continue
134 | }
135 | if ret < 0 {
136 | $if verbose ? {
137 | eprintln('[DEBUG] wait_cqe error: ${ret}')
138 | }
139 | break
140 | }
141 | dispatch_io_uring_cqe(worker, cqe, handler)
142 | C.io_uring_cqe_seen(&worker.ring, cqe)
143 | submitted := C.io_uring_submit(&worker.ring)
144 | $if verbose ? {
145 | eprintln('[DEBUG] Submitted ${submitted} SQE(s)\n')
146 | }
147 | }
148 |
149 | mut pending := 0
150 | for {
151 | mut cqe := &C.io_uring_cqe(unsafe { nil })
152 | for C.io_uring_peek_cqe(&worker.ring, &cqe) == 0 {
153 | dispatch_io_uring_cqe(worker, cqe, handler)
154 | pending++
155 | C.io_uring_cqe_seen(&worker.ring, cqe)
156 | }
157 | if pending > 0 {
158 | submitted := C.io_uring_submit(&worker.ring)
159 | $if verbose ? {
160 | eprintln('[DEBUG] Submitted ${submitted} SQE(s)')
161 | }
162 | pending = 0
163 | }
164 | }
165 | }
166 |
167 | pub fn run_io_uring_backend(request_handler fn ([]u8, int) ![]u8, port int, mut threads []thread) {
168 | num_workers := max_thread_pool_size
169 |
170 | for i in 0 .. num_workers {
171 | mut worker := &io_uring.Worker{}
172 | worker.cpu_id = i
173 | worker.socket_fd = -1
174 | io_uring.pool_init(mut worker)
175 |
176 | // Try to initialize io_uring ring with SQPOLL, fall back if it fails
177 | mut params := C.io_uring_params{}
178 | params.flags |= 1 << 3 // IORING_SETUP_SQPOLL
179 | params.sq_thread_cpu = i // Pin SQ thread to worker CPU
180 | mut sqpoll_failed := false
181 | if C.io_uring_queue_init_params(u32(io_uring.default_ring_entries), &worker.ring,
182 | ¶ms) < 0 {
183 | eprintln('[io_uring] worker ${i}: SQPOLL failed, falling back to normal io_uring')
184 | // Try again without SQPOLL
185 | params = C.io_uring_params{}
186 | if C.io_uring_queue_init_params(u32(io_uring.default_ring_entries), &worker.ring,
187 | ¶ms) < 0 {
188 | eprintln('Failed to initialize io_uring for worker ${i}')
189 | exit(1)
190 | }
191 | sqpoll_failed = true
192 | }
193 | // Use single-shot accept for SO_REUSEPORT
194 | // worker.use_multishot = false
195 | if sqpoll_failed {
196 | eprintln('[io_uring] worker ${i}: single-shot accept enabled (no SQPOLL)')
197 | } else {
198 | eprintln('[io_uring] worker ${i}: single-shot accept + SQPOLL enabled')
199 | }
200 |
201 | // Create per-worker listener
202 | worker.socket_fd = socket.create_server_socket(port)
203 | if worker.socket_fd < 0 {
204 | eprintln('Failed to create listener for worker ${i}')
205 | exit(1)
206 | }
207 | // Spawn worker thread
208 | threads[i] = spawn io_uring_worker_loop(worker, request_handler)
209 | }
210 |
211 | println('listening on http://localhost:${port}/ (io_uring)')
212 |
213 | // Keep main thread alive
214 | for {
215 | C.sleep(1)
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/http_server/http_server_epoll_linux.c.v:
--------------------------------------------------------------------------------
1 | module http_server
2 |
3 | import epoll
4 | import socket
5 | import http1_1.response
6 | import http1_1.request
7 |
8 | #include
9 | #include
10 |
11 | fn C.perror(s &u8)
12 | fn C.sleep(seconds u32) u32
13 | fn C.close(fd int) int
14 |
15 | // Handles a readable client connection: receives the request, routes it, and sends the response.
16 | fn handle_readable_fd(request_handler fn ([]u8, int) ![]u8, epoll_fd int, client_conn_fd int) {
17 | request_buffer := request.read_request(client_conn_fd) or {
18 | $if verbose ? {
19 | eprintln('[epoll-worker] Error reading request from fd ${client_conn_fd}: ${err}')
20 | }
21 | response.send_status_444_response(client_conn_fd)
22 | epoll.remove_fd_from_epoll(epoll_fd, client_conn_fd)
23 | return
24 | }
25 |
26 | defer {
27 | unsafe { request_buffer.free() }
28 | }
29 |
30 | response_buffer := request_handler(request_buffer, client_conn_fd) or {
31 | eprintln('Error handling request: ${err}')
32 | response.send_bad_request_response(client_conn_fd)
33 | epoll.remove_fd_from_epoll(epoll_fd, client_conn_fd)
34 | return
35 | }
36 |
37 | response.send_response(client_conn_fd, response_buffer.data, response_buffer.len) or {
38 | epoll.remove_fd_from_epoll(epoll_fd, client_conn_fd)
39 | return
40 | }
41 |
42 | $if force_keep_alive ? || test {
43 | // Keep the connection alive unconditionally
44 | // through -d force_keep_alive flag or during tests
45 | // tests need to be alive due to how
46 | // they are implemented in `(mut s Server) test`
47 | return
48 | } $else {
49 | mut keep_alive := false
50 | for variant in connection_keep_alive_variants {
51 | mut i := 0
52 | for i <= request_buffer.len - variant.len {
53 | if unsafe { vmemcmp(&request_buffer[i], &variant[0], variant.len) } == 0 {
54 | keep_alive = true
55 | break
56 | }
57 | // Move to next line
58 | for i < request_buffer.len && request_buffer[i] != `\n` {
59 | i++
60 | }
61 | i++
62 | }
63 | if keep_alive {
64 | break
65 | }
66 | }
67 | if !keep_alive {
68 | epoll.remove_fd_from_epoll(epoll_fd, client_conn_fd)
69 | }
70 | }
71 | }
72 |
73 | // Accept loop for the main epoll thread. Distributes new client connections to worker threads (round-robin).
74 | fn handle_accept_loop(socket_fd int, main_epoll_fd int, epoll_fds []int) {
75 | mut next_worker := 0
76 | mut event := C.epoll_event{}
77 |
78 | for {
79 | // Wait for events on the main epoll fd (listening socket)
80 | num_events := C.epoll_wait(main_epoll_fd, &event, 1, -1)
81 | $if verbose ? {
82 | eprintln('[epoll] epoll_wait returned ${num_events}')
83 | }
84 | if num_events < 0 {
85 | if C.errno == C.EINTR {
86 | continue
87 | }
88 | C.perror(c'epoll_wait')
89 | break
90 | }
91 |
92 | if num_events > 1 {
93 | eprintln('More than one event in epoll_wait, this should not happen.')
94 | continue
95 | }
96 |
97 | if event.events & u32(C.EPOLLIN) != 0 {
98 | $if verbose ? {
99 | eprintln('[epoll] EPOLLIN event on listening socket')
100 | }
101 | for {
102 | // Accept new client connection
103 | client_conn_fd := C.accept(socket_fd, C.NULL, C.NULL)
104 | $if verbose ? {
105 | println('[epoll] accept() returned ${client_conn_fd}')
106 | }
107 | if client_conn_fd < 0 {
108 | // Check for EAGAIN or EWOULDBLOCK, usually represented by errno 11.
109 | if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
110 | $if verbose ? {
111 | println('[epoll] No more incoming connections to accept (EAGAIN/EWOULDBLOCK)')
112 | }
113 | break // No more incoming connections; exit loop.
114 | }
115 | eprintln(@LOCATION)
116 | C.perror(c'Accept failed')
117 | continue
118 | }
119 | // Set client socket to non-blocking
120 | socket.set_blocking(client_conn_fd, false)
121 | // Distribute client connection to worker threads (round-robin)
122 | epoll_fd := epoll_fds[next_worker]
123 | next_worker = (next_worker + 1) % max_thread_pool_size
124 | $if verbose ? {
125 | eprintln('[epoll] Adding client fd ${client_conn_fd} to worker epoll fd ${epoll_fd}')
126 | }
127 | if epoll.add_fd_to_epoll(epoll_fd, client_conn_fd, u32(C.EPOLLIN | C.EPOLLET)) < 0 {
128 | socket.close_socket(client_conn_fd)
129 | continue
130 | }
131 | }
132 | }
133 | }
134 | }
135 |
136 | @[direct_array_access; manualfree]
137 | fn process_events(event_callbacks epoll.EpollEventCallbacks, epoll_fd int) {
138 | mut events := [socket.max_connection_size]C.epoll_event{}
139 |
140 | for {
141 | // Wait for events on the worker's epoll fd
142 | num_events := C.epoll_wait(epoll_fd, &events[0], socket.max_connection_size, -1)
143 | $if verbose ? {
144 | eprintln('[epoll-worker] epoll_wait returned ${num_events} on fd ${epoll_fd}')
145 | }
146 | if num_events < 0 {
147 | if C.errno == C.EINTR {
148 | continue
149 | }
150 | eprintln(@LOCATION)
151 | C.perror(c'epoll_wait')
152 | break
153 | }
154 |
155 | for i in 0 .. num_events {
156 | client_conn_fd := unsafe { events[i].data.fd }
157 | $if verbose ? {
158 | eprintln('[epoll-worker] Event for client fd ${client_conn_fd}: events=${events[i].events}')
159 | }
160 | // Remove fd if hangup or error
161 | if events[i].events & u32(C.EPOLLHUP | C.EPOLLERR) != 0 {
162 | println('[epoll-worker] HUP/ERR on fd ${client_conn_fd}, removing')
163 | epoll.remove_fd_from_epoll(epoll_fd, client_conn_fd)
164 | continue
165 | }
166 |
167 | // Readable event
168 | if events[i].events & u32(C.EPOLLIN) != 0 {
169 | $if verbose ? {
170 | println('[epoll-worker] EPOLLIN for fd ${client_conn_fd}, calling on_read')
171 | }
172 | event_callbacks.on_read(client_conn_fd)
173 | }
174 |
175 | // Writable event
176 | if events[i].events & u32(C.EPOLLOUT) != 0 {
177 | $if verbose ? {
178 | println('[epoll-worker] EPOLLOUT for fd ${client_conn_fd}, calling on_write')
179 | }
180 | event_callbacks.on_write(client_conn_fd)
181 | }
182 | }
183 | }
184 | }
185 |
186 | pub fn run_epoll_backend(socket_fd int, request_handler fn ([]u8, int) ![]u8, port int, mut threads []thread) {
187 | if socket_fd < 0 {
188 | return
189 | }
190 |
191 | // Create main epoll instance
192 | // the function of the main_epoll_fd is to monitor the listening socket for incoming connections
193 | // then distribute them to worker threads
194 | main_epoll_fd := epoll.create_epoll_fd()
195 | if main_epoll_fd < 0 {
196 | socket.close_socket(socket_fd)
197 | exit(1)
198 | }
199 |
200 | if epoll.add_fd_to_epoll(main_epoll_fd, socket_fd, u32(C.EPOLLIN)) < 0 {
201 | socket.close_socket(socket_fd)
202 | socket.close_socket(main_epoll_fd)
203 | exit(1)
204 | }
205 |
206 | // the function of this epoll_fds array is to hold epoll fds for each worker thread
207 | // they are used to distribute client connections among worker threads
208 | mut epoll_fds := []int{len: max_thread_pool_size, cap: max_thread_pool_size}
209 |
210 | unsafe { epoll_fds.flags.set(.noslices | .noshrink | .nogrow) }
211 | for i in 0 .. max_thread_pool_size {
212 | epoll_fds[i] = epoll.create_epoll_fd()
213 | if epoll_fds[i] < 0 {
214 | C.perror(c'epoll_create1')
215 | for j in 0 .. i {
216 | socket.close_socket(epoll_fds[j])
217 | }
218 | socket.close_socket(main_epoll_fd)
219 | socket.close_socket(socket_fd)
220 | exit(1)
221 | }
222 |
223 | // Build per-thread callbacks: default to handle_readable_fd; write is a no-op.
224 | epoll_fd := epoll_fds[i]
225 | callbacks := epoll.EpollEventCallbacks{
226 | on_read: fn [request_handler, epoll_fd] (client_conn_fd int) {
227 | handle_readable_fd(request_handler, epoll_fd, client_conn_fd)
228 | }
229 | on_write: fn (_ int) {}
230 | }
231 | threads[i] = spawn process_events(callbacks, epoll_fds[i])
232 | }
233 |
234 | println('listening on http://localhost:${port}/')
235 | handle_accept_loop(socket_fd, main_epoll_fd, epoll_fds)
236 | }
237 |
--------------------------------------------------------------------------------
/http_server/io_uring/io_uring_linux.c.v:
--------------------------------------------------------------------------------
1 | module io_uring
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #flag -luring
10 |
11 | // ==================== C Function Declarations ====================
12 |
13 | // Socket functions
14 | fn C.socket(domain int, typ int, protocol int) int
15 | fn C.setsockopt(sockfd int, level int, optname int, optval voidptr, optlen u32) int
16 | fn C.bind(sockfd int, addr voidptr, addrlen u32) int
17 | fn C.listen(sockfd int, backlog int) int
18 | fn C.close(fd int) int
19 |
20 | // Network byte order
21 | fn C.htons(hostshort u16) u16
22 | fn C.htonl(hostlong u32) u32
23 |
24 | // File control
25 | fn C.fcntl(fd int, cmd int, arg int) int
26 |
27 | // Error handling
28 | fn C.perror(s &char)
29 |
30 | // ==================== Constants ====================
31 |
32 | // Server configuration
33 | pub const inaddr_any = u32(0)
34 | pub const default_port = 8080
35 | pub const default_ring_entries = 16384
36 | pub const default_buffer_size = 4096
37 |
38 | // Derived constants
39 | pub const max_conn_per_worker = default_ring_entries * 2
40 |
41 | // Operation types for user_data encoding
42 | pub const op_accept = u8(1)
43 | pub const op_read = u8(2)
44 | pub const op_write = u8(3)
45 |
46 | // IO uring CQE flags
47 | const ioring_cqe_f_more = u32(1 << 1)
48 | // io_uring features
49 | pub const ioring_feat_accept_multishot = u32(1 << 19)
50 |
51 | // User data bit masks
52 | const op_type_shift = 48
53 | const ptr_mask = u64(0x0000FFFFFFFFFFFF)
54 |
55 | // ==================== User Data Encoding ====================
56 | // Encoding scheme: [63:48]=op type, [47:0]=pointer value
57 | // This allows storing both operation type and connection pointer in a single u64
58 |
59 | @[inline]
60 | pub fn encode_user_data(op u8, ptr voidptr) u64 {
61 | return (u64(op) << op_type_shift) | u64(ptr)
62 | }
63 |
64 | @[inline]
65 | pub fn decode_op_type(data u64) u8 {
66 | return u8(data >> op_type_shift)
67 | }
68 |
69 | @[inline]
70 | pub fn decode_connection_ptr(data u64) voidptr {
71 | return voidptr(data & ptr_mask)
72 | }
73 |
74 | // ==================== C Bindings ====================
75 |
76 | // io_uring structures and functions
77 | pub struct C.io_uring {
78 | mu int
79 | cq int
80 | sq int
81 | ring_fd int
82 | compat int
83 | int_flags u32
84 | pad [1]u8
85 | enter_ring_fd int
86 | }
87 |
88 | pub struct C.io_uring_sqe {}
89 |
90 | pub struct C.io_uring_cqe {
91 | user_data u64
92 | res i32
93 | flags u32
94 | }
95 |
96 | // Simplified params with features field for capability detection
97 | pub struct C.io_uring_params {
98 | flags u32
99 | sq_thread_cpu u32
100 | sq_thread_idle u32
101 | features u32
102 | }
103 |
104 | pub struct C.cpu_set_t {
105 | val [16]u64
106 | }
107 |
108 | // C function bindings
109 | fn C.io_uring_queue_init_params(entries u32, ring &C.io_uring, p &C.io_uring_params) int
110 | fn C.io_uring_queue_exit(ring &C.io_uring)
111 | fn C.io_uring_get_sqe(ring &C.io_uring) &C.io_uring_sqe
112 | fn C.io_uring_prep_accept(sqe &C.io_uring_sqe, fd int, addr voidptr, addrlen voidptr, flags int)
113 | fn C.io_uring_prep_multishot_accept(sqe &C.io_uring_sqe, fd int, addr voidptr, addrlen voidptr, flags int)
114 | fn C.io_uring_sqe_set_data64(sqe &C.io_uring_sqe, data u64)
115 | fn C.io_uring_prep_recv(sqe &C.io_uring_sqe, fd int, buf voidptr, nbytes usize, flags int)
116 | fn C.io_uring_prep_send(sqe &C.io_uring_sqe, fd int, buf voidptr, nbytes usize, flags int)
117 | fn C.io_uring_submit(ring &C.io_uring) int
118 | fn C.io_uring_wait_cqe(ring &C.io_uring, cqe_ptr &&C.io_uring_cqe) int
119 | fn C.io_uring_peek_cqe(ring &C.io_uring, cqe_ptr &&C.io_uring_cqe) int
120 | fn C.io_uring_cqe_seen(ring &C.io_uring, cqe &C.io_uring_cqe)
121 | fn C.io_uring_cqe_get_data64(cqe &C.io_uring_cqe) u64
122 |
123 | // htonl function converts a u_long from host to TCP/IP network byte order (which is big-endian).
124 | // htonl() function converts the unsigned long integer hostlong from host byte order to network byte order.
125 | fn C.htonl(hostlong u32) u32
126 |
127 | @[typedef]
128 | pub struct C.pthread_t {
129 | data u64
130 | }
131 |
132 | @[typedef]
133 | pub struct C.sigaction {
134 | sa_handler voidptr
135 | sa_mask u64
136 | sa_flags int
137 | sa_restorer voidptr
138 | }
139 |
140 | // ==================== Connection Structure ====================
141 |
142 | // Represents a client connection with request/response state
143 | pub struct Connection {
144 | pub mut:
145 | // Socket file descriptor
146 | fd int
147 | // Backpointer to owning worker (for pool management)
148 | owner &Worker = unsafe { nil }
149 |
150 | // Request state
151 | buf [default_buffer_size]u8
152 | bytes_read int
153 |
154 | // Response state
155 | response_buffer []u8
156 | bytes_sent int
157 |
158 | // Processing flag
159 | processing bool
160 | }
161 |
162 | // ==================== Worker Structure ====================
163 |
164 | pub struct Worker {
165 | pub mut:
166 | ring C.io_uring
167 | cpu_id int
168 | tid C.pthread_t
169 | socket_fd int
170 | use_multishot bool
171 | verbose bool
172 | conns []Connection
173 | free_stack []int
174 | free_top int
175 | }
176 |
177 | // ==================== Connection Pool ====================
178 |
179 | // Initialize connection pool for a worker
180 | pub fn pool_init(mut w Worker) {
181 | // Pre-allocate all connections
182 | w.conns = []Connection{len: max_conn_per_worker, init: Connection{}}
183 | w.free_stack = []int{len: max_conn_per_worker}
184 | w.free_top = 0
185 |
186 | // Initialize free list (all slots available)
187 | for i in 0 .. max_conn_per_worker {
188 | w.free_stack[w.free_top] = i
189 | w.free_top++
190 | }
191 | }
192 |
193 | // Check if pool has available connections
194 | @[inline]
195 | fn pool_has_capacity(w &Worker) bool {
196 | return w.free_top > 0
197 | }
198 |
199 | pub fn pool_acquire(mut w Worker, fd int) &Connection {
200 | if w.free_top == 0 {
201 | return unsafe { nil }
202 | }
203 | w.free_top--
204 | idx := w.free_stack[w.free_top]
205 | mut c := &w.conns[idx]
206 | c.fd = fd
207 | unsafe {
208 | c.owner = &w
209 | }
210 | c.bytes_read = 0
211 | c.bytes_sent = 0
212 | c.processing = false
213 | unsafe { c.response_buffer.free() }
214 | c.response_buffer = []u8{}
215 | return c
216 | }
217 |
218 | pub fn pool_release(mut w Worker, mut c Connection) {
219 | if unsafe { c.owner == nil } {
220 | return
221 | }
222 | C.close(c.fd)
223 | unsafe { c.response_buffer.free() }
224 | c.response_buffer = []u8{}
225 | c.bytes_read = 0
226 | c.bytes_sent = 0
227 | c.processing = false
228 | unsafe {
229 | idx := int(u64(&c) - u64(&w.conns[0])) / int(sizeof(Connection))
230 | if w.free_top < max_conn_per_worker {
231 | w.free_stack[w.free_top] = idx
232 | w.free_top++
233 | }
234 | }
235 | }
236 |
237 | // Wrapper functions that work with const pointers
238 | pub fn pool_acquire_from_ptr(worker &Worker, fd int) &Connection {
239 | mut w := unsafe { &Worker(worker) }
240 | return pool_acquire(mut w, fd)
241 | }
242 |
243 | pub fn pool_release_from_ptr(worker &Worker, mut c Connection) {
244 | mut w := unsafe { &Worker(worker) }
245 | pool_release(mut w, mut c)
246 | }
247 |
248 | // ==================== Socket Configuration ====================
249 |
250 | pub fn tune_socket(fd int) {
251 | flags := C.fcntl(fd, C.F_GETFL, 0)
252 | C.fcntl(fd, C.F_SETFL, flags | C.O_NONBLOCK)
253 |
254 | one := 1
255 | C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_NODELAY, &one, sizeof(int))
256 |
257 | sndbuf := 524288 // 512KB
258 | C.setsockopt(fd, C.SOL_SOCKET, C.SO_SNDBUF, &sndbuf, sizeof(int))
259 |
260 | rcvbuf := 262144 // 256KB
261 | C.setsockopt(fd, C.SOL_SOCKET, C.SO_RCVBUF, &rcvbuf, sizeof(int))
262 | }
263 |
264 | // ==================== IO Uring Operations ====================
265 |
266 | // Prepare accept operation (multishot when supported)
267 | // Returns true if SQE was successfully obtained, false otherwise
268 | pub fn prepare_accept(ring &C.io_uring, socket_fd int, multishot bool) bool {
269 | sqe := C.io_uring_get_sqe(ring)
270 | if unsafe { sqe == nil } {
271 | return false
272 | }
273 | if multishot {
274 | C.io_uring_prep_multishot_accept(sqe, socket_fd, unsafe { nil }, unsafe { nil },
275 | C.SOCK_NONBLOCK)
276 | } else {
277 | C.io_uring_prep_accept(sqe, socket_fd, unsafe { nil }, unsafe { nil }, 0)
278 | }
279 | C.io_uring_sqe_set_data64(sqe, encode_user_data(op_accept, unsafe { nil }))
280 | return true
281 | }
282 |
283 | // Prepare receive operation for a connection
284 | pub fn prepare_recv(ring &C.io_uring, mut c Connection) bool {
285 | sqe := C.io_uring_get_sqe(ring)
286 | if unsafe { sqe == nil } {
287 | return false
288 | }
289 | C.io_uring_prep_recv(sqe, c.fd, unsafe { &c.buf[0] }, default_buffer_size, 0)
290 | C.io_uring_sqe_set_data64(sqe, encode_user_data(op_read, &c))
291 | return true
292 | }
293 |
294 | // Prepare send operation for a connection
295 | pub fn prepare_send(ring &C.io_uring, mut c Connection, data &u8, data_len usize) bool {
296 | sqe := C.io_uring_get_sqe(ring)
297 | if unsafe { sqe == nil } {
298 | return false
299 | }
300 | C.io_uring_prep_send(sqe, c.fd, unsafe { data }, data_len, 0)
301 | C.io_uring_sqe_set_data64(sqe, encode_user_data(op_write, &c))
302 | return true
303 | }
304 |
305 | // ==================== Type Definitions ====================
306 |
307 | pub type WorkerFn = fn (&Worker) voidptr
308 |
--------------------------------------------------------------------------------
/http_server/http_server_windows.c.v:
--------------------------------------------------------------------------------
1 | module http_server
2 |
3 | import socket
4 | import runtime
5 | import iocp
6 | import http1_1.response
7 | import http1_1.request
8 |
9 | // Backend selection
10 | pub enum IOBackend {
11 | iocp = 0 // Windows only
12 | }
13 |
14 | #include
15 | #include
16 |
17 | $if windows {
18 | #include
19 | }
20 |
21 | const acceptex_guid = C.WSAID_ACCEPTEX
22 | const max_iocp_workers = runtime.nr_cpus()
23 | const buffer_size = 8192
24 |
25 | struct WorkerContext {
26 | pub mut:
27 | iocp_handle voidptr
28 | handler fn ([]u8, int) ![]u8 @[required]
29 | running bool
30 | thread_id u32
31 | }
32 |
33 | struct AcceptContext {
34 | pub mut:
35 | listen_socket int
36 | iocp_handle voidptr
37 | accept_socket int
38 | overlapped C.OVERLAPPED
39 | local_addr [sizeof(C.sockaddr_in) + 16]u8
40 | remote_addr [sizeof(C.sockaddr_in) + 16]u8
41 | }
42 |
43 | fn get_system_error() string {
44 | error_code := C.WSAGetLastError()
45 | mut buffer := [256]u16{}
46 | C.FormatMessageW(C.FORMAT_MESSAGE_FROM_SYSTEM | C.FORMAT_MESSAGE_IGNORE_INSERTS, unsafe { nil },
47 | error_code, 0, &buffer[0], 256, unsafe { nil })
48 | return string_from_wide(&buffer[0])
49 | }
50 |
51 | fn worker_thread(mut ctx WorkerContext) {
52 | println('[iocp-worker] Worker thread started')
53 |
54 | for ctx.running {
55 | mut bytes_transferred := u32(0)
56 | mut completion_key := u64(0)
57 | mut overlapped := &C.OVERLAPPED(unsafe { nil })
58 |
59 | success := iocp.get_queued_completion_status(ctx.iocp_handle, &bytes_transferred,
60 | &completion_key, &overlapped, iocp.infinity)
61 |
62 | if !success {
63 | // Check if it's a shutdown signal
64 | if overlapped == unsafe { nil } && bytes_transferred == 0 && completion_key == 0 {
65 | println('[iocp-worker] Received shutdown signal')
66 | break
67 | }
68 | error_code := C.WSAGetLastError()
69 | if error_code == 64 { // WAIT_TIMEOUT
70 | continue
71 | }
72 | eprintln('[iocp-worker] GetQueuedCompletionStatus failed: ${get_system_error()}')
73 | continue
74 | }
75 |
76 | // Handle shutdown signal
77 | if bytes_transferred == 0 && completion_key == 0 && overlapped == unsafe { nil } {
78 | println('[iocp-worker] Received shutdown signal')
79 | break
80 | }
81 |
82 | // Cast overlapped back to IOData
83 | io_data := unsafe { &iocp.IOData(overlapped) }
84 |
85 | match io_data.operation {
86 | .accept {
87 | handle_accept_completion(io_data, ctx.handler, mut ctx)
88 | }
89 | .read {
90 | handle_read_completion(io_data, ctx.handler, mut ctx)
91 | }
92 | .write {
93 | handle_write_completion(io_data, mut ctx)
94 | }
95 | .close {
96 | socket.close_socket(io_data.socket_fd)
97 | iocp.free_io_data(io_data)
98 | }
99 | }
100 | }
101 |
102 | println('[iocp-worker] Worker thread exiting')
103 | }
104 |
105 | fn handle_accept_completion(io_data &iocp.IOData, handler fn ([]u8, int) ![]u8,
106 | mut ctx WorkerContext) {
107 | socket_fd := io_data.socket_fd
108 |
109 | // Set socket options for accepted connection
110 | opt := 1
111 | C.setsockopt(u64(socket_fd), C.SOL_SOCKET, C.SO_UPDATE_ACCEPT_CONTEXT, &socket_fd,
112 | sizeof(socket_fd))
113 |
114 | // Associate the accepted socket with IOCP
115 | iocp.associate_handle_with_iocp(ctx.iocp_handle, socket_fd, u64(socket_fd)) or {
116 | eprintln('[iocp-worker] Failed to associate accepted socket: ${err}')
117 | socket.close_socket(socket_fd)
118 | return
119 | }
120 |
121 | // Post a read operation on the new socket
122 | post_read_operation(socket_fd, ctx.iocp_handle)
123 |
124 | // Post another accept on the listening socket
125 | // (We need to get the listening socket from somewhere - stored in context)
126 | }
127 |
128 | fn handle_read_completion(io_data &iocp.IOData, handler fn ([]u8, int) ![]u8,
129 | mut ctx WorkerContext) {
130 | socket_fd := io_data.socket_fd
131 | bytes_read := io_data.bytes_transferred
132 |
133 | if bytes_read == 0 {
134 | // Connection closed
135 | socket.close_socket(socket_fd)
136 | iocp.free_io_data(io_data)
137 | return
138 | }
139 |
140 | // Process the request
141 | request_data := io_data.buffer[..bytes_read]
142 | response_data := handler(request_data, socket_fd) or {
143 | response.send_bad_request_response(socket_fd)
144 | socket.close_socket(socket_fd)
145 | iocp.free_io_data(io_data)
146 | return
147 | }
148 |
149 | // Prepare write operation
150 | write_io_data := iocp.create_io_data(socket_fd, .write, response_data.len)
151 | unsafe {
152 | C.memcpy(&write_io_data.buffer[0], response_data.data, response_data.len)
153 | }
154 | write_io_data.wsabuf.len = u32(response_data.len)
155 |
156 | // Post write operation
157 | flags := u32(0)
158 | result := iocp.post_send(socket_fd, &write_io_data.wsabuf, 1, flags, &write_io_data.overlapped)
159 |
160 | if result == socket.socket_error {
161 | error_code := C.WSAGetLastError()
162 | if error_code != 997 { // ERROR_IO_PENDING
163 | eprintln('[iocp-worker] WSASend failed: ${get_system_error()}')
164 | socket.close_socket(socket_fd)
165 | iocp.free_io_data(write_io_data)
166 | }
167 | }
168 |
169 | // Free read IO data
170 | iocp.free_io_data(io_data)
171 |
172 | // Prepare for next read (keep-alive)
173 | // In a real implementation, we'd parse the request to determine if keep-alive
174 | // For simplicity, we always post another read
175 | post_read_operation(socket_fd, ctx.iocp_handle)
176 | }
177 |
178 | fn handle_write_completion(io_data &iocp.IOData, mut ctx WorkerContext) {
179 | socket_fd := io_data.socket_fd
180 |
181 | // Check if all data was sent
182 | if io_data.bytes_transferred < io_data.wsabuf.len {
183 | // Partial write, adjust buffer and repost
184 | remaining := io_data.wsabuf.len - io_data.bytes_transferred
185 | unsafe {
186 | C.memmove(&io_data.buffer[0], &io_data.buffer[io_data.bytes_transferred],
187 | remaining)
188 | }
189 | io_data.wsabuf.len = remaining
190 |
191 | flags := u32(0)
192 | result := iocp.post_send(socket_fd, &io_data.wsabuf, 1, flags, &io_data.overlapped)
193 |
194 | if result == socket.socket_error {
195 | error_code := C.WSAGetLastError()
196 | if error_code != 997 { // ERROR_IO_PENDING
197 | socket.close_socket(socket_fd)
198 | iocp.free_io_data(io_data)
199 | }
200 | }
201 | } else {
202 | // Write completed
203 | iocp.free_io_data(io_data)
204 | // Note: We don't close the socket here to allow keep-alive
205 | // The next read operation was already posted in handle_read_completion
206 | }
207 | }
208 |
209 | fn post_read_operation(socket_fd int, iocp_handle voidptr) {
210 | read_io_data := iocp.create_io_data(socket_fd, .read, buffer_size)
211 |
212 | flags := u32(0)
213 | result := iocp.post_recv(socket_fd, &read_io_data.wsabuf, 1, &flags, &read_io_data.overlapped)
214 |
215 | if result == socket.socket_error {
216 | error_code := C.WSAGetLastError()
217 | if error_code != 997 { // ERROR_IO_PENDING
218 | eprintln('[iocp-worker] WSARecv failed: ${get_system_error()}')
219 | socket.close_socket(socket_fd)
220 | iocp.free_io_data(read_io_data)
221 | }
222 | }
223 | }
224 |
225 | fn accept_thread(listen_socket int, iocp_handle voidptr) {
226 | println('[iocp-accept] Accept thread started')
227 |
228 | for {
229 | // Create a new socket for the incoming connection
230 | accept_socket := int(C.socket(C.AF_INET, C.SOCK_STREAM, 0))
231 | if accept_socket == int(socket.invalid_socket) {
232 | eprintln('[iocp-accept] Failed to create accept socket')
233 | C.sleep(100)
234 | continue
235 | }
236 |
237 | // Prepare accept IO data
238 | accept_io_data := iocp.create_io_data(accept_socket, .accept, 0)
239 |
240 | // Start asynchronous accept
241 | if !iocp.start_accept_ex(listen_socket, accept_socket, &accept_io_data.overlapped) {
242 | error_code := C.WSAGetLastError()
243 | if error_code != 997 { // ERROR_IO_PENDING
244 | eprintln('[iocp-accept] AcceptEx failed: ${get_system_error()}')
245 | socket.close_socket(accept_socket)
246 | iocp.free_io_data(accept_io_data)
247 | C.sleep(100)
248 | continue
249 | }
250 | }
251 |
252 | // Wait for accept completion
253 | // In a real implementation, we'd use IOCP for accepts too
254 | // For simplicity, we're using AcceptEx synchronously here
255 | // A better implementation would post multiple AcceptEx operations
256 | result := C.WaitForSingleObject(voidptr(accept_io_data.overlapped.h_event), iocp.infinity)
257 |
258 | if result == 0 { // WAIT_OBJECT_0
259 | bytes_received := u32(0)
260 | C.GetOverlappedResult(voidptr(listen_socket), &accept_io_data.overlapped,
261 | &bytes_received, false)
262 |
263 | // Associate with IOCP and post read
264 | iocp.associate_handle_with_iocp(iocp_handle, accept_socket, u64(accept_socket)) or {
265 | eprintln('[iocp-accept] Failed to associate accepted socket: ${err}')
266 | socket.close_socket(accept_socket)
267 | iocp.free_io_data(accept_io_data)
268 | continue
269 | }
270 |
271 | post_read_operation(accept_socket, iocp_handle)
272 | iocp.free_io_data(accept_io_data)
273 | } else {
274 | socket.close_socket(accept_socket)
275 | iocp.free_io_data(accept_io_data)
276 | }
277 | }
278 |
279 | println('[iocp-accept] Accept thread exiting')
280 | }
281 |
282 | pub fn run_iocp_backend(socket_fd int, handler fn ([]u8, int) ![]u8, port int, mut threads []thread) {
283 | println('[iocp] Starting IOCP backend on port ${port}')
284 |
285 | // Create IOCP handle
286 | iocp_handle := iocp.create_iocp(max_iocp_workers) or {
287 | eprintln('[iocp] Failed to create IOCP: ${err}')
288 | return
289 | }
290 |
291 | // Associate listening socket with IOCP
292 | iocp.associate_handle_with_iocp(iocp_handle, socket_fd, u64(socket_fd)) or {
293 | eprintln('[iocp] Failed to associate listening socket: ${err}')
294 | return
295 | }
296 |
297 | // Create worker threads
298 | mut worker_contexts := []&WorkerContext{cap: max_iocp_workers}
299 | for i in 0 .. max_iocp_workers {
300 | mut ctx := &WorkerContext{
301 | iocp_handle: iocp_handle
302 | handler: handler
303 | running: true
304 | }
305 | worker_contexts << ctx
306 | threads[i] = spawn worker_thread(mut ctx)
307 | println('[iocp] Started worker thread ${i}')
308 | }
309 |
310 | // Start accept thread
311 | accept_thread_id := spawn accept_thread(socket_fd, iocp_handle)
312 |
313 | println('listening on http://localhost:${port}/ (IOCP)')
314 |
315 | // Wait for shutdown signal (in real implementation, this would be controlled)
316 | mut dummy := 0
317 | C.scanf('%d', &dummy)
318 |
319 | // Shutdown all workers
320 | for mut ctx in worker_contexts {
321 | ctx.running = false
322 | iocp.post_iocp_status(iocp_handle, 0, 0, unsafe { nil })
323 | }
324 |
325 | // Wait for workers to finish
326 | for i in 0 .. max_iocp_workers {
327 | threads[i].wait()
328 | }
329 |
330 | // Close IOCP handle
331 | C.CloseHandle(iocp_handle)
332 |
333 | println('[iocp] Server stopped')
334 | }
335 |
336 | pub fn (mut server Server) run() {
337 | $if windows {
338 | run_iocp_backend(server.socket_fd, server.request_handler, server.port, mut server.threads)
339 | } $else {
340 | eprintln('Windows IOCP backend only works on Windows')
341 | exit(1)
342 | }
343 | }
344 |
--------------------------------------------------------------------------------