File Coverage

blib/lib/Hypersonic/Socket.pm
Criterion Covered Total %
statement 41 49 83.6
branch 5 18 27.7
condition 1 2 50.0
subroutine 10 11 90.9
pod 2 4 50.0
total 59 84 70.2


line stmt bran cond sub pod time code
1             package Hypersonic::Socket;
2              
3 41     41   657908 use strict;
  41         58  
  41         1232  
4 41     41   193 use warnings;
  41         56  
  41         1960  
5 41     41   703 use 5.010;
  41         121  
6              
7             our $VERSION = '0.17';
8              
9 41     41   2828 use XS::JIT;
  41         6038  
  41         944  
10 41     41   3496 use XS::JIT::Builder;
  41         8383  
  41         1624  
11 41     41   17299 use Hypersonic::JIT::Util;
  41         109  
  41         36070  
12              
13             # Platform detection
14             sub platform {
15 7 50   7 1 452533 return 'darwin' if $^O eq 'darwin';
16 7 50       82 return 'linux' if $^O eq 'linux';
17 0 0       0 return 'freebsd' if $^O eq 'freebsd';
18 0 0       0 return 'openbsd' if $^O eq 'openbsd';
19 0 0       0 return 'netbsd' if $^O eq 'netbsd';
20 0 0       0 return 'mswin32' if $^O eq 'MSWin32';
21 0 0       0 return 'cygwin' if $^O eq 'cygwin';
22 0         0 die "Unsupported platform: $^O";
23             }
24              
25             # Event backend detection (delegates to Hypersonic::Event)
26             sub event_backend {
27 1     1 0 709 require Hypersonic::Event;
28 1         6 return Hypersonic::Event->best_backend();
29             }
30              
31             my $COMPILED = 0;
32             my $MODULE_ID = 0;
33              
34             # Unified compile interface
35             sub compile {
36 0     0 0 0 my ($class, %opts) = @_;
37 0         0 return $class->compile_socket_ops(%opts);
38             }
39              
40             # Generate and compile JIT socket functions using Builder
41             sub compile_socket_ops {
42 42     42 1 116 my ($class, %opts) = @_;
43              
44 42 100       170 return 1 if $COMPILED;
45              
46 41   50     232 my $cache_dir = $opts{cache_dir} // '_hypersonic_cache/socket';
47 41         105 my $module_name = 'Hypersonic::Socket::Ops_' . $MODULE_ID++;
48              
49 41         367 my $builder = XS::JIT::Builder->new;
50              
51             # Common includes via centralized utility
52 41         169 Hypersonic::JIT::Util->add_standard_includes($builder,
53             qw(stdio unistd fcntl socket));
54              
55 41         317 $builder->line('#define RECV_BUF_SIZE 65536')
56             ->blank
57             ->line('static char recv_buf[RECV_BUF_SIZE];')
58             ->blank;
59              
60             # Windows: WSAStartup must run before any socket call. We don't
61             # have a BOOT hook in the JIT module, so guard with a static flag
62             # and call it from create_listen_socket (the first entrypoint
63             # users hit). socket() requires Winsock to be initialized; otherwise
64             # it returns INVALID_SOCKET with WSANOTINITIALISED.
65 41         114 $builder->raw(<<'C');
66             #ifdef _WIN32
67             static int hs_wsa_initialized = 0;
68             static void hs_wsa_init(void) {
69             if (!hs_wsa_initialized) {
70             WSADATA wsa;
71             if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
72             return; /* leave flag unset; create_listen_socket will croak */
73             }
74             hs_wsa_initialized = 1;
75             }
76             }
77             #else
78             static inline void hs_wsa_init(void) {}
79             #endif
80             C
81              
82             # Generate create_listen_socket
83 41         13155 $builder->xs_function('jit_create_listen_socket')
84             ->xs_preamble
85             ->line('IV port;')
86             ->line('int fd;')
87             ->line('int opt;')
88             ->line('struct sockaddr_in addr;')
89             ->blank
90             ->line('if (items != 1) croak("Usage: create_listen_socket(port)");')
91             ->line('port = SvIV(ST(0));')
92             ->blank
93             ->line('hs_wsa_init(); /* no-op on POSIX */')
94             ->line('fd = socket(AF_INET, SOCK_STREAM, 0);')
95             ->if('fd < 0')
96             # Surface the actual errno - returning silent -1 hides why the
97             # child server died on platforms where bind/listen/socket fail
98             # for non-obvious reasons (see OpenBSD smoke reports).
99             ->line('croak("socket() failed: %s", strerror(errno));')
100             ->endif
101             ->blank
102             ->line('opt = 1;')
103             ->line('setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));')
104             ->line('#ifdef SO_REUSEPORT')
105             ->line('setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, (const char*)&opt, sizeof(opt));')
106             ->line('#endif')
107             ->blank
108             ->line('hs_set_nonblocking(fd);')
109             ->blank
110             ->line('memset(&addr, 0, sizeof(addr));')
111             ->line('addr.sin_family = AF_INET;')
112             ->line('addr.sin_port = htons((uint16_t)port);')
113             ->line('addr.sin_addr.s_addr = INADDR_ANY;')
114             ->blank
115             ->if('bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0')
116             ->line('int saved_errno = errno;')
117             ->line('close(fd);')
118             ->line('croak("bind(port=%d) failed: %s", (int)port, strerror(saved_errno));')
119             ->endif
120             ->blank
121             ->if('listen(fd, SOMAXCONN) < 0')
122             ->line('int saved_errno = errno;')
123             ->line('close(fd);')
124             ->line('croak("listen() failed: %s", strerror(saved_errno));')
125             ->endif
126             ->blank
127             ->line('ST(0) = sv_2mortal(newSViv(fd));')
128             ->xs_return('1')
129             ->xs_end;
130              
131             # Event loop functions (create_event_loop, event_add, event_del, ev_poll)
132             # have been moved to Hypersonic::Event::* backend modules
133              
134             # Generate http_accept
135 41         1305 $builder->xs_function('jit_http_accept')
136             ->xs_preamble
137             ->line('if (items != 1) croak("Usage: http_accept(listen_fd)");')
138             ->line('IV listen_fd = SvIV(ST(0));')
139             ->blank
140             ->line('struct sockaddr_in client_addr;')
141             ->line('socklen_t client_len = sizeof(client_addr);')
142             ->blank
143             ->line('int client_fd = accept((int)listen_fd, (struct sockaddr*)&client_addr, &client_len);')
144             ->blank
145             ->if('client_fd < 0')
146             ->line('ST(0) = sv_2mortal(newSViv(-1));')
147             ->line('XSRETURN(1);')
148             ->endif
149             ->blank
150             ->comment('Portable non-blocking: hs_set_nonblocking expands to')
151             ->comment('ioctlsocket(FIONBIO) on Win32 and fcntl(F_SETFL|O_NONBLOCK)')
152             ->comment('on POSIX. Defined by Hypersonic::JIT::Util::add_standard_includes.')
153             ->line('hs_set_nonblocking(client_fd);')
154             ->blank
155             ->line('ST(0) = sv_2mortal(newSViv(client_fd));')
156             ->xs_return('1')
157             ->xs_end;
158              
159             # Generate http_recv - zero-copy HTTP parsing
160 41         3141 $builder->xs_function('jit_http_recv')
161             ->xs_preamble
162             ->line('if (items != 1) croak("Usage: http_recv(fd)");')
163             ->line('IV fd = SvIV(ST(0));')
164             ->blank
165             ->line('ssize_t len = recv((int)fd, recv_buf, RECV_BUF_SIZE - 1, 0);')
166             ->blank
167             ->if('len <= 0')
168             ->line('ST(0) = &PL_sv_undef;')
169             ->line('XSRETURN(1);')
170             ->endif
171             ->blank
172             ->line('recv_buf[len] = \'\\0\';')
173             ->blank
174             ->comment('Quick parse - extract method, path, detect keep-alive')
175             ->line('const char* p = recv_buf;')
176             ->line('const char* end = recv_buf + len;')
177             ->blank
178             ->comment('Method')
179             ->line('const char* method = p;')
180             ->line('while (p < end && *p != \' \') p++;')
181             ->line('int method_len = p - method;')
182             ->if('p >= end')
183             ->line('ST(0) = &PL_sv_undef;')
184             ->line('XSRETURN(1);')
185             ->endif
186             ->line('p++;')
187             ->blank
188             ->comment('Path')
189             ->line('const char* path = p;')
190             ->line('while (p < end && *p != \' \' && *p != \'?\') p++;')
191             ->line('int path_len = p - path;')
192             ->if('p >= end')
193             ->line('ST(0) = &PL_sv_undef;')
194             ->line('XSRETURN(1);')
195             ->endif
196             ->blank
197             ->comment('Skip to end of request line')
198             ->line('while (p < end && *p != \'\\n\') p++;')
199             ->if('p >= end')
200             ->line('ST(0) = &PL_sv_undef;')
201             ->line('XSRETURN(1);')
202             ->endif
203             ->line('p++;')
204             ->blank
205             ->comment('Check for Connection: close')
206             ->line('int keep_alive = 1;')
207             ->line('while (p < end) {')
208             ->line(' if (*p == \'\\r\' || *p == \'\\n\') break;')
209             ->line(' if (end - p > 17 && strncasecmp(p, "Connection: close", 17) == 0) {')
210             ->line(' keep_alive = 0;')
211             ->line(' }')
212             ->line(' while (p < end && *p != \'\\n\') p++;')
213             ->line(' if (p < end) p++;')
214             ->line('}')
215             ->blank
216             ->comment('Skip blank line')
217             ->line('if (p < end && *p == \'\\r\') p++;')
218             ->line('if (p < end && *p == \'\\n\') p++;')
219             ->blank
220             ->comment('Body')
221             ->line('const char* body = p;')
222             ->line('int body_len = end - p;')
223             ->blank
224             ->comment('Build request array: [method, path, body, keep_alive, fd]')
225             ->line('AV* req = newAV();')
226             ->line('av_push(req, newSVpvn(method, method_len));')
227             ->line('av_push(req, newSVpvn(path, path_len));')
228             ->line('av_push(req, newSVpvn(body, body_len));')
229             ->line('av_push(req, newSViv(keep_alive));')
230             ->line('av_push(req, newSViv(fd));')
231             ->blank
232             ->line('ST(0) = sv_2mortal(newRV_noinc((SV*)req));')
233             ->xs_return('1')
234             ->xs_end;
235              
236             # Generate http_send - writev for zero-copy
237 41         1171 $builder->xs_function('jit_http_send')
238             ->xs_preamble
239             ->line('if (items < 2 || items > 3) croak("Usage: http_send(fd, body, [content_type])");')
240             ->line('IV fd = SvIV(ST(0));')
241             ->blank
242             ->line('STRLEN body_len;')
243             ->line('const char* body = SvPV(ST(1), body_len);')
244             ->blank
245             ->line('const char* content_type = "text/plain";')
246             ->if('items == 3 && SvOK(ST(2))')
247             ->line('STRLEN ct_len;')
248             ->line('content_type = SvPV(ST(2), ct_len);')
249             ->endif
250             ->blank
251             ->line('static __thread char header[512];')
252             ->line('int hdr_len = snprintf(header, sizeof(header),')
253             ->line(' "HTTP/1.1 200 OK\\r\\n"')
254             ->line(' "Content-Type: %s\\r\\n"')
255             ->line(' "Content-Length: %zu\\r\\n"')
256             ->line(' "Connection: keep-alive\\r\\n\\r\\n",')
257             ->line(' content_type, body_len);')
258             ->blank
259             # writev() / struct iovec are POSIX-only (sys/uio.h). On Windows
260             # we'd have to use WSASend with WSABUF[]; for simplicity (and
261             # because Win32 perl smokers run mostly correctness, not perf,
262             # tests) just emit two send() calls. Header is always <512 bytes
263             # so the kernel-buffer copy is negligible.
264             ->raw(<<'C')
265             #ifdef _WIN32
266             ssize_t sent = send((int)fd, header, (size_t)hdr_len, 0);
267             if (sent > 0 && body_len > 0) {
268             ssize_t sent2 = send((int)fd, body, (size_t)body_len, 0);
269             if (sent2 > 0) sent += sent2;
270             }
271             #else
272             struct iovec iov[2];
273             iov[0].iov_base = header;
274             iov[0].iov_len = (size_t)hdr_len;
275             iov[1].iov_base = (void*)body;
276             iov[1].iov_len = body_len;
277             ssize_t sent = writev((int)fd, iov, 2);
278             #endif
279             C
280             ->line('ST(0) = sv_2mortal(newSViv((IV)sent));')
281             ->xs_return('1')
282             ->xs_end;
283              
284             # Generate http_send_404
285 41         499 $builder->xs_function('jit_http_send_404')
286             ->xs_preamble
287             ->line('if (items != 1) croak("Usage: http_send_404(fd)");')
288             ->line('IV fd = SvIV(ST(0));')
289             ->blank
290             ->line('static const char resp[] =')
291             ->line(' "HTTP/1.1 404 Not Found\\r\\n"')
292             ->line(' "Content-Type: text/plain\\r\\n"')
293             ->line(' "Content-Length: 9\\r\\n"')
294             ->line(' "Connection: close\\r\\n\\r\\n"')
295             ->line(' "Not Found";')
296             ->blank
297             ->line('ssize_t sent = send((int)fd, resp, sizeof(resp) - 1, 0);')
298             ->line('ST(0) = sv_2mortal(newSViv((IV)sent));')
299             ->xs_return('1')
300             ->xs_end;
301              
302             # Generate close_fd
303 41         296 $builder->xs_function('jit_close_fd')
304             ->xs_preamble
305             ->line('if (items != 1) croak("Usage: close_fd(fd)");')
306             ->line('IV fd = SvIV(ST(0));')
307             ->line('int result = close((int)fd);')
308             ->line('ST(0) = sv_2mortal(newSViv(result));')
309             ->xs_return('1')
310             ->xs_end;
311              
312             # Compile via XS::JIT (socket-only functions - event loop is in backends)
313 41 50       1466634 XS::JIT->compile(
314             code => $builder->code,
315             name => $module_name,
316             cache_dir => $cache_dir,
317             # Windows needs to link against Winsock for the JIT-compiled .so
318             # to resolve socket()/recv()/send()/etc.
319             ($^O eq 'MSWin32' ? (extra_ldflags => '-lws2_32') : ()),
320             functions => {
321             'Hypersonic::Socket::create_listen_socket' => { source => 'jit_create_listen_socket', is_xs_native => 1 },
322             'Hypersonic::Socket::http_accept' => { source => 'jit_http_accept', is_xs_native => 1 },
323             'Hypersonic::Socket::http_recv' => { source => 'jit_http_recv', is_xs_native => 1 },
324             'Hypersonic::Socket::http_send' => { source => 'jit_http_send', is_xs_native => 1 },
325             'Hypersonic::Socket::http_send_404' => { source => 'jit_http_send_404', is_xs_native => 1 },
326             'Hypersonic::Socket::close_fd' => { source => 'jit_close_fd', is_xs_native => 1 },
327             },
328             );
329              
330 41         356 $COMPILED = 1;
331 41         464465 return 1;
332             }
333              
334             # Auto-compile on import
335             sub import {
336 42     42   1363 my $class = shift;
337 42         95 my %opts = @_;
338 42         114 $class->compile_socket_ops(%opts);
339             }
340              
341             1;
342              
343             __END__