├── 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 | vanilla Logo 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 | --------------------------------------------------------------------------------