File Coverage

lib/Hypersonic/WebSocket/Handler.pm
Criterion Covered Total %
statement 70 70 100.0
branch n/a
condition 3 4 75.0
subroutine 19 19 100.0
pod 0 16 0.0
total 92 109 84.4


line stmt bran cond sub pod time code
1             package Hypersonic::WebSocket::Handler;
2 1     1   1185 use strict;
  1         1  
  1         38  
3 1     1   4 use warnings;
  1         1  
  1         55  
4              
5             # Hypersonic::WebSocket::Handler - XS WebSocket connection management
6             #
7             # All connection management is done in C via XS::JIT::Builder.
8             # This module generates XS functions callable from Perl.
9             # Object-oriented API: $conn = Handler->new($fd, $ws); $conn->send($msg);
10              
11             our $VERSION = '0.15';
12              
13             # Maximum concurrent WebSocket connections
14 1     1   3 use constant MAX_CONNECTIONS => 65536;
  1         1  
  1         1901  
15              
16             # Generate all Handler XS code
17             sub generate_c_code {
18 6     6 0 25925 my ($class, $builder, $opts) = @_;
19 6   100     31 $opts //= {};
20            
21 6   50     33 my $max_conns = $opts->{max_connections} // MAX_CONNECTIONS;
22            
23             # Generate connection registry (static C)
24 6         27 $class->gen_connection_registry($builder, $max_conns);
25            
26             # Generate XS functions - instance methods
27 6         23 $class->gen_xs_new($builder);
28 6         24 $class->gen_xs_fd($builder);
29 6         15 $class->gen_xs_state($builder);
30 6         16 $class->gen_xs_is_open($builder);
31 6         11 $class->gen_xs_ws($builder);
32 6         14 $class->gen_xs_send($builder);
33 6         41 $class->gen_xs_send_binary($builder);
34 6         17 $class->gen_xs_handle_data($builder);
35 6         15 $class->gen_xs_close($builder);
36            
37             # Class methods
38 6         12 $class->gen_xs_count($builder);
39 6         34 $class->gen_xs_get($builder);
40 6         11 $class->gen_xs_is_websocket($builder);
41 6         19 $class->gen_xs_broadcast($builder);
42            
43 6         13 return $builder;
44             }
45              
46             # Generate connection registry (C data structures)
47             sub gen_connection_registry {
48 6     6 0 14 my ($class, $builder, $max_conns) = @_;
49            
50 6         165 $builder->comment('WebSocket connection registry')
51             ->line('#define WS_MAX_CONNECTIONS ' . $max_conns)
52             ->blank
53             ->comment('Connection states')
54             ->line('#define WS_STATE_INIT 0')
55             ->line('#define WS_STATE_OPEN 1')
56             ->line('#define WS_STATE_CLOSING 2')
57             ->line('#define WS_STATE_CLOSED 3')
58             ->blank
59             ->comment('Connection structure')
60             ->line('typedef struct {')
61             ->line(' int active;')
62             ->line(' int state;')
63             ->line(' SV* ws_object;')
64             ->line(' SV* handler;')
65             ->line('} WSConnection;')
66             ->blank
67             ->line('static WSConnection ws_handler_registry[WS_MAX_CONNECTIONS];')
68             ->line('static int ws_connection_count = 0;')
69             ->blank;
70            
71 6         14 return $builder;
72             }
73              
74             # XS: new($fd, $ws) - create connection, return blessed object
75             sub gen_xs_new {
76 6     6 0 12 my ($class, $builder) = @_;
77            
78 6         274 $builder->xs_function('xs_ws_new')
79             ->xs_preamble
80             ->line('int fd;')
81             ->line('SV* ws;')
82             ->line('WSConnection* conn;')
83             ->line('SV* fd_sv;')
84             ->line('SV* fd_ref;')
85             ->blank
86             ->if('items != 3')
87             ->line('croak("Usage: Hypersonic::WebSocket::Handler->new(fd, ws)");')
88             ->endif
89             ->blank
90             ->line('fd = SvIV(ST(1));')
91             ->line('ws = ST(2);')
92             ->blank
93             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
94             ->line('croak("fd out of range: %d", fd);')
95             ->endif
96             ->blank
97             ->line('conn = &ws_handler_registry[fd];')
98             ->if('conn->active')
99             ->comment('Already registered, return existing')
100             ->else
101             ->line('conn->active = 1;')
102             ->line('conn->state = WS_STATE_OPEN;')
103             ->line('conn->ws_object = SvREFCNT_inc(ws);')
104             ->line('conn->handler = NULL;')
105             ->line('ws_connection_count++;')
106             ->endif
107             ->blank
108             ->comment('Create blessed object: bless \\$fd, class')
109             ->line('fd_sv = newSViv(fd);')
110             ->line('fd_ref = newRV_noinc(fd_sv);')
111             ->line('sv_bless(fd_ref, gv_stashpv("Hypersonic::WebSocket::Handler", GV_ADD));')
112             ->line('ST(0) = sv_2mortal(fd_ref);')
113             ->line('XSRETURN(1);')
114             ->xs_end
115             ->blank;
116            
117 6         14 return $builder;
118             }
119              
120             # XS: fd() - get fd from object
121             sub gen_xs_fd {
122 6     6 0 11 my ($class, $builder) = @_;
123            
124 6         80 $builder->xs_function('xs_ws_fd')
125             ->xs_preamble
126             ->line('int fd;')
127             ->blank
128             ->if('items != 1')
129             ->line('croak("Usage: $conn->fd()");')
130             ->endif
131             ->blank
132             ->line('fd = SvIV(SvRV(ST(0)));')
133             ->line('XSRETURN_IV(fd);')
134             ->xs_end
135             ->blank;
136            
137 6         7 return $builder;
138             }
139              
140             # XS: state() - get connection state
141             sub gen_xs_state {
142 6     6 0 8 my ($class, $builder) = @_;
143            
144 6         90 $builder->xs_function('xs_ws_state')
145             ->xs_preamble
146             ->line('int fd;')
147             ->blank
148             ->if('items != 1')
149             ->line('croak("Usage: $conn->state()");')
150             ->endif
151             ->blank
152             ->line('fd = SvIV(SvRV(ST(0)));')
153             ->blank
154             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
155             ->line('XSRETURN_IV(-1);')
156             ->endif
157             ->if('!ws_handler_registry[fd].active')
158             ->line('XSRETURN_IV(-1);')
159             ->endif
160             ->blank
161             ->line('XSRETURN_IV(ws_handler_registry[fd].state);')
162             ->xs_end
163             ->blank;
164            
165 6         8 return $builder;
166             }
167              
168             # XS: is_open() - check if connection is open
169             sub gen_xs_is_open {
170 6     6 0 9 my ($class, $builder) = @_;
171            
172 6         94 $builder->xs_function('xs_ws_is_open')
173             ->xs_preamble
174             ->if('items != 1')
175             ->line('croak("Usage: $conn->is_open()");')
176             ->endif
177             ->blank
178             ->line('int fd = SvIV(SvRV(ST(0)));')
179             ->blank
180             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
181             ->line('XSRETURN_NO;')
182             ->endif
183             ->if('!ws_handler_registry[fd].active')
184             ->line('XSRETURN_NO;')
185             ->endif
186             ->blank
187             ->if('ws_handler_registry[fd].state == WS_STATE_OPEN')
188             ->line('XSRETURN_YES;')
189             ->else
190             ->line('XSRETURN_NO;')
191             ->endif
192             ->xs_end
193             ->blank;
194            
195 6         6 return $builder;
196             }
197              
198             # XS: ws() - get WebSocket object
199             sub gen_xs_ws {
200 6     6 0 8 my ($class, $builder) = @_;
201            
202 6         83 $builder->xs_function('xs_ws_ws')
203             ->xs_preamble
204             ->if('items != 1')
205             ->line('croak("Usage: $conn->ws()");')
206             ->endif
207             ->blank
208             ->line('int fd = SvIV(SvRV(ST(0)));')
209             ->blank
210             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
211             ->line('XSRETURN_UNDEF;')
212             ->endif
213             ->if('!ws_handler_registry[fd].active')
214             ->line('XSRETURN_UNDEF;')
215             ->endif
216             ->blank
217             ->line('ST(0) = ws_handler_registry[fd].ws_object;')
218             ->line('XSRETURN(1);')
219             ->xs_end
220             ->blank;
221            
222 6         6 return $builder;
223             }
224              
225             # XS: send($message) - send text frame
226             sub gen_xs_send {
227 6     6 0 7 my ($class, $builder) = @_;
228            
229 6         163 $builder->xs_function('xs_ws_send')
230             ->xs_preamble
231             ->if('items != 2')
232             ->line('croak("Usage: $conn->send(message)");')
233             ->endif
234             ->blank
235             ->line('int fd = SvIV(SvRV(ST(0)));')
236             ->line('STRLEN msg_len;')
237             ->line('const char* message = SvPV(ST(1), msg_len);')
238             ->blank
239             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
240             ->line('XSRETURN_NO;')
241             ->endif
242             ->if('!ws_handler_registry[fd].active || ws_handler_registry[fd].state != WS_STATE_OPEN')
243             ->line('XSRETURN_NO;')
244             ->endif
245             ->blank
246             ->line('uint8_t frame[65546];')
247             ->line('size_t frame_len = ws_encode_text(frame, sizeof(frame), message, msg_len);')
248             ->blank
249             ->if('frame_len == 0')
250             ->line('XSRETURN_NO;')
251             ->endif
252             ->blank
253             ->line('send(fd, frame, frame_len, 0);')
254             ->line('XSRETURN_YES;')
255             ->xs_end
256             ->blank;
257            
258 6         6 return $builder;
259             }
260              
261             # XS: send_binary($data) - send binary frame
262             sub gen_xs_send_binary {
263 6     6 0 9 my ($class, $builder) = @_;
264            
265 6         128 $builder->xs_function('xs_ws_send_binary')
266             ->xs_preamble
267             ->if('items != 2')
268             ->line('croak("Usage: $conn->send_binary(data)");')
269             ->endif
270             ->blank
271             ->line('int fd = SvIV(SvRV(ST(0)));')
272             ->line('STRLEN data_len;')
273             ->line('const char* data = SvPV(ST(1), data_len);')
274             ->blank
275             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
276             ->line('XSRETURN_NO;')
277             ->endif
278             ->if('!ws_handler_registry[fd].active || ws_handler_registry[fd].state != WS_STATE_OPEN')
279             ->line('XSRETURN_NO;')
280             ->endif
281             ->blank
282             ->line('uint8_t frame[65546];')
283             ->line('size_t frame_len = ws_encode_binary(frame, sizeof(frame), (const uint8_t*)data, data_len);')
284             ->blank
285             ->if('frame_len == 0')
286             ->line('XSRETURN_NO;')
287             ->endif
288             ->blank
289             ->line('send(fd, frame, frame_len, 0);')
290             ->line('XSRETURN_YES;')
291             ->xs_end
292             ->blank;
293            
294 6         7 return $builder;
295             }
296              
297             # XS: handle_data($data) - process incoming frame
298             sub gen_xs_handle_data {
299 6     6 0 8 my ($class, $builder) = @_;
300            
301 6         123 $builder->xs_function('xs_ws_handle_data')
302             ->xs_preamble
303             ->if('items != 2')
304             ->line('croak("Usage: $conn->handle_data(data)");')
305             ->endif
306             ->blank
307             ->line('int fd = SvIV(SvRV(ST(0)));')
308             ->line('STRLEN data_len;')
309             ->line('const uint8_t* data = (const uint8_t*)SvPV(ST(1), data_len);')
310             ->blank
311             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
312             ->line('XSRETURN_UNDEF;')
313             ->endif
314             ->if('!ws_handler_registry[fd].active')
315             ->line('XSRETURN_UNDEF;')
316             ->endif
317             ->blank
318             ->comment('Decode WebSocket frame')
319             ->line('WSFrame frame;')
320             ->line('int result = ws_decode_frame(data, data_len, &frame);')
321             ->blank
322             ->if('result <= 0')
323             ->line('XSRETURN_UNDEF;')
324             ->endif
325             ->blank
326             ->comment('Return decoded message')
327             ->line('HV* hv = newHV();')
328             ->line('hv_store(hv, "opcode", 6, newSViv(frame.opcode), 0);')
329             ->line('hv_store(hv, "data", 4, newSVpvn((char*)frame.payload, frame.payload_length), 0);')
330             ->line('ST(0) = sv_2mortal(newRV_noinc((SV*)hv));')
331             ->line('XSRETURN(1);')
332             ->xs_end
333             ->blank;
334            
335 6         7 return $builder;
336             }
337              
338             # XS: close([$code]) - close connection
339             sub gen_xs_close {
340 6     6 0 8 my ($class, $builder) = @_;
341            
342 6         141 $builder->xs_function('xs_ws_close')
343             ->xs_preamble
344             ->if('items < 1')
345             ->line('croak("Usage: $conn->close([code])");')
346             ->endif
347             ->blank
348             ->line('int fd = SvIV(SvRV(ST(0)));')
349             ->line('int code = (items >= 2) ? SvIV(ST(1)) : 1000;')
350             ->blank
351             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
352             ->line('XSRETURN_NO;')
353             ->endif
354             ->if('!ws_handler_registry[fd].active')
355             ->line('XSRETURN_NO;')
356             ->endif
357             ->blank
358             ->line('WSConnection* conn = &ws_handler_registry[fd];')
359             ->blank
360             ->comment('Send close frame if connection is open')
361             ->if('conn->state == WS_STATE_OPEN')
362             ->line('uint8_t close_frame[4];')
363             ->line('close_frame[0] = 0x88;')
364             ->line('close_frame[1] = 2;')
365             ->line('close_frame[2] = (code >> 8) & 0xFF;')
366             ->line('close_frame[3] = code & 0xFF;')
367             ->line('send(fd, close_frame, 4, 0);')
368             ->line('conn->state = WS_STATE_CLOSING;')
369             ->endif
370             ->blank
371             ->comment('Cleanup')
372             ->if('conn->ws_object')
373             ->line('SvREFCNT_dec(conn->ws_object);')
374             ->endif
375             ->if('conn->handler')
376             ->line('SvREFCNT_dec(conn->handler);')
377             ->endif
378             ->line('memset(conn, 0, sizeof(WSConnection));')
379             ->line('ws_connection_count--;')
380             ->blank
381             ->line('XSRETURN_YES;')
382             ->xs_end
383             ->blank;
384            
385 6         22 return $builder;
386             }
387              
388             # ============================================================
389             # Class Methods
390             # ============================================================
391              
392             # XS: count() - class method, total connection count
393             sub gen_xs_count {
394 6     6 0 8 my ($class, $builder) = @_;
395            
396 6         27 $builder->xs_function('xs_ws_count')
397             ->xs_preamble
398             ->line('XSRETURN_IV(ws_connection_count);')
399             ->xs_end
400             ->blank;
401            
402 6         6 return $builder;
403             }
404              
405             # XS: get($fd) - class method, get Handler by fd
406             sub gen_xs_get {
407 6     6 0 9 my ($class, $builder) = @_;
408            
409 6         90 $builder->xs_function('xs_ws_get')
410             ->xs_preamble
411             ->if('items != 2')
412             ->line('croak("Usage: Hypersonic::WebSocket::Handler->get(fd)");')
413             ->endif
414             ->blank
415             ->line('int fd = SvIV(ST(1));')
416             ->blank
417             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
418             ->line('XSRETURN_UNDEF;')
419             ->endif
420             ->if('!ws_handler_registry[fd].active')
421             ->line('XSRETURN_UNDEF;')
422             ->endif
423             ->blank
424             ->comment('Return blessed handler object')
425             ->line('SV* fd_sv = newSViv(fd);')
426             ->line('SV* fd_ref = newRV_noinc(fd_sv);')
427             ->line('sv_bless(fd_ref, gv_stashpv("Hypersonic::WebSocket::Handler", GV_ADD));')
428             ->line('ST(0) = sv_2mortal(fd_ref);')
429             ->line('XSRETURN(1);')
430             ->xs_end
431             ->blank;
432            
433 6         6 return $builder;
434             }
435              
436             # XS: is_websocket($fd) - class method
437             sub gen_xs_is_websocket {
438 6     6 0 8 my ($class, $builder) = @_;
439            
440 6         85 $builder->xs_function('xs_ws_is_websocket')
441             ->xs_preamble
442             ->if('items != 2')
443             ->line('croak("Usage: Hypersonic::WebSocket::Handler->is_websocket(fd)");')
444             ->endif
445             ->blank
446             ->line('int fd = SvIV(ST(1));')
447             ->blank
448             ->if('fd < 0 || fd >= WS_MAX_CONNECTIONS')
449             ->line('XSRETURN_NO;')
450             ->endif
451             ->blank
452             ->if('ws_handler_registry[fd].active')
453             ->line('XSRETURN_YES;')
454             ->else
455             ->line('XSRETURN_NO;')
456             ->endif
457             ->xs_end
458             ->blank;
459            
460 6         6 return $builder;
461             }
462              
463             # XS: broadcast($message, [$exclude]) - class method
464             sub gen_xs_broadcast {
465 6     6 0 9 my ($class, $builder) = @_;
466            
467 6         242 $builder->xs_function('xs_ws_broadcast')
468             ->xs_preamble
469             ->if('items < 2')
470             ->line('croak("Usage: Hypersonic::WebSocket::Handler->broadcast(message, [exclude])");')
471             ->endif
472             ->blank
473             ->line('STRLEN msg_len;')
474             ->line('const char* message = SvPV(ST(1), msg_len);')
475             ->line('int exclude_fd = -1;')
476             ->line('int fd;')
477             ->blank
478             ->comment('Handle exclude - can be fd or Handler object')
479             ->if('items >= 3')
480             ->if('SvROK(ST(2))')
481             ->line('SV* deref = SvRV(ST(2));')
482             ->if('SvTYPE(deref) == SVt_PVHV')
483             ->comment('WebSocket object with fd key')
484             ->line('HV* hv = (HV*)deref;')
485             ->line('SV** fd_sv = hv_fetchs(hv, "fd", 0);')
486             ->if('fd_sv && *fd_sv')
487             ->line('exclude_fd = SvIV(*fd_sv);')
488             ->endif
489             ->else
490             ->comment('Handler object (blessed scalar ref)')
491             ->line('exclude_fd = SvIV(deref);')
492             ->endif
493             ->else
494             ->line('exclude_fd = SvIV(ST(2));')
495             ->endif
496             ->endif
497             ->blank
498             ->comment('Encode as WebSocket text frame')
499             ->line('uint8_t frame[65546];')
500             ->line('size_t frame_len = ws_encode_text(frame, sizeof(frame), message, msg_len);')
501             ->if('frame_len == 0')
502             ->line('XSRETURN_IV(0);')
503             ->endif
504             ->blank
505             ->line('int sent = 0;')
506             ->for('fd = 0', 'fd < WS_MAX_CONNECTIONS', 'fd++')
507             ->if('ws_handler_registry[fd].active && ws_handler_registry[fd].state == WS_STATE_OPEN && fd != exclude_fd')
508             ->line('send(fd, frame, frame_len, 0);')
509             ->line('sent++;')
510             ->endif
511             ->endfor
512             ->blank
513             ->line('XSRETURN_IV(sent);')
514             ->xs_end
515             ->blank;
516            
517 6         8 return $builder;
518             }
519              
520             # Get XS function mappings for XS::JIT->compile
521             sub get_xs_functions {
522             return {
523             # Instance methods
524 1     1 0 47 'Hypersonic::WebSocket::Handler::new' => { source => 'xs_ws_new', is_xs_native => 1 },
525             'Hypersonic::WebSocket::Handler::fd' => { source => 'xs_ws_fd', is_xs_native => 1 },
526             'Hypersonic::WebSocket::Handler::state' => { source => 'xs_ws_state', is_xs_native => 1 },
527             'Hypersonic::WebSocket::Handler::is_open' => { source => 'xs_ws_is_open', is_xs_native => 1 },
528             'Hypersonic::WebSocket::Handler::ws' => { source => 'xs_ws_ws', is_xs_native => 1 },
529             'Hypersonic::WebSocket::Handler::send' => { source => 'xs_ws_send', is_xs_native => 1 },
530             'Hypersonic::WebSocket::Handler::send_binary' => { source => 'xs_ws_send_binary', is_xs_native => 1 },
531             'Hypersonic::WebSocket::Handler::handle_data' => { source => 'xs_ws_handle_data', is_xs_native => 1 },
532             'Hypersonic::WebSocket::Handler::close' => { source => 'xs_ws_close', is_xs_native => 1 },
533             # Class methods
534             'Hypersonic::WebSocket::Handler::count' => { source => 'xs_ws_count', is_xs_native => 1 },
535             'Hypersonic::WebSocket::Handler::get' => { source => 'xs_ws_get', is_xs_native => 1 },
536             'Hypersonic::WebSocket::Handler::is_websocket' => { source => 'xs_ws_is_websocket', is_xs_native => 1 },
537             'Hypersonic::WebSocket::Handler::broadcast' => { source => 'xs_ws_broadcast', is_xs_native => 1 },
538             };
539             }
540              
541             1;
542              
543             __END__