From 8f72ea1a8fb2f83dc990c3c4f51c6584eca0cf8f Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 14 Jan 2026 01:02:21 +0300 Subject: [PATCH] fasthttp,veb: static files via sendfile --- examples/fasthttp/main.v | 18 ++++-- vlib/fasthttp/fasthttp.v | 8 ++- vlib/fasthttp/fasthttp_darwin.v | 98 +++++++++++++++++++++++++++----- vlib/fasthttp/fasthttp_linux.v | 38 ++++++++++--- vlib/fasthttp/fasthttp_test.v | 6 +- vlib/fasthttp/fasthttp_windows.v | 2 +- vlib/veb/veb_d_new_veb.v | 18 +++++- 7 files changed, 152 insertions(+), 36 deletions(-) diff --git a/examples/fasthttp/main.v b/examples/fasthttp/main.v index 136f592c9..6b8cdc301 100644 --- a/examples/fasthttp/main.v +++ b/examples/fasthttp/main.v @@ -2,24 +2,32 @@ module main import fasthttp -fn handle_request(req fasthttp.HttpRequest) ![]u8 { +fn handle_request(req fasthttp.HttpRequest) !fasthttp.HttpResponse { method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr() path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr() if method == 'GET' { if path == '/' { - return home_controller()! + return fasthttp.HttpResponse{ + content: home_controller()! + } } else if path.starts_with('/user/') { id := path[6..] - return get_user_controller(id)! + return fasthttp.HttpResponse{ + content: get_user_controller(id)! + } } } else if method == 'POST' { if path == '/user' { - return create_user_controller()! + return fasthttp.HttpResponse{ + content: create_user_controller()! + } } } - return not_found_response()! + return fasthttp.HttpResponse{ + content: not_found_response()! + } } fn main() { diff --git a/vlib/fasthttp/fasthttp.v b/vlib/fasthttp/fasthttp.v index 9856898a8..82c207e08 100644 --- a/vlib/fasthttp/fasthttp.v +++ b/vlib/fasthttp/fasthttp.v @@ -62,11 +62,17 @@ pub mut: user_data voidptr // User-defined context data } +pub struct HttpResponse { +pub: + content []u8 + file_path string +} + // ServerConfig bundles the parameters needed to start a fasthttp server. pub struct ServerConfig { pub: port int = 3000 max_request_buffer_size int = 8192 - handler fn (HttpRequest) ![]u8 @[required] + handler fn (HttpRequest) !HttpResponse @[required] user_data voidptr } diff --git a/vlib/fasthttp/fasthttp_darwin.v b/vlib/fasthttp/fasthttp_darwin.v index bbf5d0253..6aa2c6bcf 100644 --- a/vlib/fasthttp/fasthttp_darwin.v +++ b/vlib/fasthttp/fasthttp_darwin.v @@ -3,6 +3,10 @@ module fasthttp import net #include +#include +#include +#include +#include const buf_size = max_connection_size const kqueue_max_events = 128 @@ -10,6 +14,10 @@ const backlog = max_connection_size fn C.kevent(kq int, changelist &C.kevent, nchanges int, eventlist &C.kevent, nevents int, timeout &C.timespec) int fn C.kqueue() int +fn C.fstat(fd int, buf &C.stat) int + +// int sendfile(int fd, int s, off_t offset, off_t *len, struct sf_hdtr *hdtr, int flags); +fn C.sendfile(fd int, s int, offset i64, len &i64, hdtr voidptr, flags int) int struct C.kevent { ident u64 @@ -38,6 +46,11 @@ mut: read_len int write_buf []u8 write_pos int + + // Sendfile state + file_fd int = -1 + file_len i64 + file_pos i64 } pub struct Server { @@ -46,7 +59,7 @@ pub mut: socket_fd int poll_fd int // kqueue fd user_data voidptr - request_handler fn (HttpRequest) ![]u8 @[required] + request_handler fn (HttpRequest) !HttpResponse @[required] } // new_server creates and initializes a new Server instance. @@ -87,27 +100,67 @@ fn close_conn(kq int, c_ptr voidptr) { if c.write_buf.len > 0 { c.write_buf.clear() } + if c.file_fd != -1 { + C.close(c.file_fd) + c.file_fd = -1 + } unsafe { free(c_ptr) } } fn send_pending(c_ptr voidptr) bool { mut c := unsafe { &Conn(c_ptr) } - if c.write_buf.len == 0 { - return false - } - remaining := c.write_buf.len - c.write_pos - if remaining <= 0 { - return false + + // 1. Send memory buffer (headers or small response) + if c.write_pos < c.write_buf.len { + remaining := c.write_buf.len - c.write_pos + write_ptr := unsafe { &c.write_buf[0] + c.write_pos } + sent := C.send(c.fd, write_ptr, remaining, 0) + if sent > 0 { + c.write_pos += int(sent) + } + if sent < 0 { + if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK { + return true + } + return false // Error + } } - write_ptr := unsafe { &c.write_buf[0] + c.write_pos } - sent := C.send(c.fd, write_ptr, remaining, 0) - if sent > 0 { - c.write_pos += int(sent) + + // 2. Send file if buffer is fully sent + if c.write_pos >= c.write_buf.len && c.file_fd != -1 { + mut len := i64(0) // Input 0 means send until EOF + ret := C.sendfile(c.file_fd, c.fd, c.file_pos, &len, unsafe { nil }, 0) + + if len > 0 { + c.file_pos += len + } + + if ret == -1 { + if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK { + return true + } + // Error sending file + C.close(c.file_fd) + c.file_fd = -1 + return false + } + + if c.file_pos >= c.file_len { + // Done sending file + C.close(c.file_fd) + c.file_fd = -1 + } else { + // Not done yet + return true + } } - if sent < 0 && (C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK) { - return true + + // Check if completely done (both buffer and file) + if c.write_pos >= c.write_buf.len && c.file_fd == -1 { + return false // Done } - return c.write_pos < c.write_buf.len + + return true // Still pending (partial buffer write or file not done) } fn send_bad_request(fd int) { @@ -169,7 +222,21 @@ fn handle_read(mut s Server, kq int, c_ptr voidptr) { return } - c.write_buf = resp.clone() + c.write_buf = resp.content.clone() + if resp.file_path != '' { + fd := C.open(resp.file_path.str, C.O_RDONLY) + if fd != -1 { + mut st := C.stat{} + if C.fstat(fd, &st) == 0 { + c.file_fd = fd + c.file_len = st.st_size + c.file_pos = 0 + } else { + C.close(fd) + } + } + } + c.write_pos = 0 c.read_len = 0 @@ -196,6 +263,7 @@ fn accept_clients(kq int, listen_fd int) { mut c := &Conn{ fd: client_fd user_data: unsafe { nil } + file_fd: -1 } add_event(kq, u64(client_fd), i16(C.EVFILT_READ), u16(C.EV_ADD | C.EV_ENABLE | C.EV_CLEAR), c) diff --git a/vlib/fasthttp/fasthttp_linux.v b/vlib/fasthttp/fasthttp_linux.v index 40115667a..1226f8411 100644 --- a/vlib/fasthttp/fasthttp_linux.v +++ b/vlib/fasthttp/fasthttp_linux.v @@ -3,6 +3,8 @@ module fasthttp import net #include +#include +#include #include fn C.accept4(sockfd int, addr &net.Addr, addrlen &u32, flags int) int @@ -13,6 +15,10 @@ fn C.epoll_ctl(__epfd int, __op int, __fd int, __event &C.epoll_event) int fn C.epoll_wait(__epfd int, __events &C.epoll_event, __maxevents int, __timeout int) int +fn C.sendfile(out_fd int, in_fd int, offset &int, count usize) int + +fn C.fstat(fd int, buf &C.stat) int + union C.epoll_data { ptr voidptr fd int @@ -34,7 +40,7 @@ mut: listen_fds []int = []int{len: max_thread_pool_size, cap: max_thread_pool_size} epoll_fds []int = []int{len: max_thread_pool_size, cap: max_thread_pool_size} threads []thread = []thread{len: max_thread_pool_size, cap: max_thread_pool_size} - request_handler fn (HttpRequest) ![]u8 @[required] + request_handler fn (HttpRequest) !HttpResponse @[required] } // new_server creates and initializes a new Server instance. @@ -242,20 +248,34 @@ fn process_events(mut server Server, epoll_fd int, listen_fd int) { } decoded_http_request.client_conn_fd = client_fd decoded_http_request.user_data = server.user_data - response_buffer := server.request_handler(decoded_http_request) or { + response := server.request_handler(decoded_http_request) or { eprintln('Error handling request ${err}') C.send(client_fd, tiny_bad_request_response.data, tiny_bad_request_response.len, C.MSG_NOSIGNAL) handle_client_closure(epoll_fd, client_fd) continue } - // Send response - sent := C.send(client_fd, response_buffer.data, response_buffer.len, - C.MSG_NOSIGNAL | C.MSG_DONTWAIT) - if sent < 0 && C.errno != C.EAGAIN && C.errno != C.EWOULDBLOCK { - eprintln('ERROR: send() failed with errno=${C.errno}') - handle_client_closure(epoll_fd, client_fd) - continue + // Send response content (headers/body) + if response.content.len > 0 { + sent := C.send(client_fd, response.content.data, response.content.len, + C.MSG_NOSIGNAL | C.MSG_DONTWAIT) + if sent < 0 && C.errno != C.EAGAIN && C.errno != C.EWOULDBLOCK { + eprintln('ERROR: send() failed with errno=${C.errno}') + handle_client_closure(epoll_fd, client_fd) + continue + } + } + + // Send file if present + if response.file_path != '' { + fd := C.open(response.file_path.str, C.O_RDONLY) + if fd != -1 { + mut st := C.stat{} + C.fstat(fd, &st) + offset := 0 + C.sendfile(client_fd, fd, &offset, usize(st.st_size)) + C.close(fd) + } } // Leave the connection open; closure is driven by client FIN or errors } else if bytes_read == 0 { diff --git a/vlib/fasthttp/fasthttp_test.v b/vlib/fasthttp/fasthttp_test.v index 1d9d4aa6e..0bde841a6 100644 --- a/vlib/fasthttp/fasthttp_test.v +++ b/vlib/fasthttp/fasthttp_test.v @@ -96,8 +96,10 @@ fn test_decode_http_request() { } fn test_new_server() { - handler := fn (req HttpRequest) ![]u8 { - return 'HTTP/1.1 200 OK\r\n\r\nHello'.bytes() + handler := fn (req HttpRequest) !HttpResponse { + return HttpResponse{ + content: 'HTTP/1.1 200 OK\r\n\r\nHello'.bytes() + } } server := new_server(ServerConfig{ diff --git a/vlib/fasthttp/fasthttp_windows.v b/vlib/fasthttp/fasthttp_windows.v index 757fceaef..5476a8052 100644 --- a/vlib/fasthttp/fasthttp_windows.v +++ b/vlib/fasthttp/fasthttp_windows.v @@ -4,7 +4,7 @@ struct Server { pub: port int = 3000 mut: - request_handler fn (HttpRequest) ![]u8 @[required] + request_handler fn (HttpRequest) !HttpResponse @[required] } // new_server creates and initializes a new Server instance. diff --git a/vlib/veb/veb_d_new_veb.v b/vlib/veb/veb_d_new_veb.v index 53a7b83a0..37825b830 100644 --- a/vlib/veb/veb_d_new_veb.v +++ b/vlib/veb/veb_d_new_veb.v @@ -51,7 +51,7 @@ pub fn run_new[A, X](mut global_app A, port int) ! { server.run() or { panic(err) } } -fn parallel_request_handler[A, X](req fasthttp.HttpRequest) ![]u8 { +fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpResponse { // Get parameters from user_data - copy to avoid use-after-free params := unsafe { *(&RequestParams(req.user_data)) } mut global_app := unsafe { &A(params.global_app) } @@ -61,7 +61,9 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) ![]u8 { s := req.buffer.bytestr() // Parse the raw request bytes into a standard `http.Request`. req2 := http.parse_request_str(s.clone()) or { - return 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + return fasthttp.HttpResponse{ + content: 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + } } // Create and populate the `veb.Context`. completed_context := handle_request_and_route[A, X](mut global_app, req2, client_fd, @@ -70,8 +72,18 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) ![]u8 { if completed_context.takeover { eprintln('[veb] WARNING: ctx.takeover_conn() was called, but this is not supported by this server backend. The connection will be closed after this response.') } + + if completed_context.return_type == .file { + return fasthttp.HttpResponse{ + content: completed_context.res.bytes() + file_path: completed_context.return_file + } + } + // The fasthttp server expects a complete response buffer to be returned. - return completed_context.res.bytes() + return fasthttp.HttpResponse{ + content: completed_context.res.bytes() + } } // handle_request_and_route is a unified function that creates the context, // runs middleware, and finds the correct route for a request. -- 2.39.5