iodine 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of iodine might be problematic. Click here for more details.

Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +63 -100
  4. data/bin/raw-rbhttp +12 -7
  5. data/examples/config.ru +8 -7
  6. data/examples/echo.ru +8 -7
  7. data/examples/info.md +41 -35
  8. data/examples/pubsub_engine.ru +12 -12
  9. data/examples/redis.ru +10 -12
  10. data/examples/shootout.ru +19 -42
  11. data/exe/iodine +116 -1
  12. data/ext/iodine/defer.c +1 -1
  13. data/ext/iodine/facil.c +12 -8
  14. data/ext/iodine/facil.h +2 -2
  15. data/ext/iodine/iodine.c +177 -343
  16. data/ext/iodine/iodine.h +18 -72
  17. data/ext/iodine/iodine_caller.c +132 -0
  18. data/ext/iodine/iodine_caller.h +21 -0
  19. data/ext/iodine/iodine_connection.c +841 -0
  20. data/ext/iodine/iodine_connection.h +55 -0
  21. data/ext/iodine/iodine_defer.c +391 -0
  22. data/ext/iodine/iodine_defer.h +7 -0
  23. data/ext/iodine/{rb-fiobj2rb.h → iodine_fiobj2rb.h} +6 -6
  24. data/ext/iodine/iodine_helpers.c +51 -5
  25. data/ext/iodine/iodine_helpers.h +2 -3
  26. data/ext/iodine/iodine_http.c +284 -141
  27. data/ext/iodine/iodine_http.h +2 -2
  28. data/ext/iodine/iodine_json.c +13 -13
  29. data/ext/iodine/iodine_json.h +1 -1
  30. data/ext/iodine/iodine_pubsub.c +573 -823
  31. data/ext/iodine/iodine_pubsub.h +15 -27
  32. data/ext/iodine/{rb-rack-io.c → iodine_rack_io.c} +30 -8
  33. data/ext/iodine/{rb-rack-io.h → iodine_rack_io.h} +1 -0
  34. data/ext/iodine/iodine_store.c +136 -0
  35. data/ext/iodine/iodine_store.h +20 -0
  36. data/ext/iodine/iodine_tcp.c +385 -0
  37. data/ext/iodine/iodine_tcp.h +9 -0
  38. data/lib/iodine.rb +73 -171
  39. data/lib/iodine/connection.rb +34 -0
  40. data/lib/iodine/pubsub.rb +5 -18
  41. data/lib/iodine/rack_utils.rb +43 -0
  42. data/lib/iodine/version.rb +1 -1
  43. data/lib/rack/handler/iodine.rb +1 -182
  44. metadata +17 -18
  45. data/ext/iodine/iodine_protocol.c +0 -689
  46. data/ext/iodine/iodine_protocol.h +0 -13
  47. data/ext/iodine/iodine_websockets.c +0 -550
  48. data/ext/iodine/iodine_websockets.h +0 -17
  49. data/ext/iodine/rb-call.c +0 -156
  50. data/ext/iodine/rb-call.h +0 -70
  51. data/ext/iodine/rb-defer.c +0 -124
  52. data/ext/iodine/rb-registry.c +0 -150
  53. data/ext/iodine/rb-registry.h +0 -34
  54. data/lib/iodine/cli.rb +0 -89
  55. data/lib/iodine/monkeypatch.rb +0 -46
  56. data/lib/iodine/protocol.rb +0 -42
  57. data/lib/iodine/websocket.rb +0 -16
@@ -1,77 +1,23 @@
1
1
  #ifndef H_IODINE_H
2
2
  #define H_IODINE_H
3
- /*
4
- Copyright: Boaz segev, 2016-2017
5
- License: MIT
6
3
 
7
- Feel free to copy, use and enjoy according to the license provided.
8
- */
9
- #include <ruby.h>
10
- #ifndef _GNU_SOURCE
11
- #define _GNU_SOURCE
12
- #endif
13
- #include <ruby/encoding.h>
14
- #include <ruby/io.h>
15
- #include <ruby/thread.h>
16
- #include <ruby/version.h>
17
-
18
- #include "rb-call.h"
19
- #include "rb-registry.h"
20
-
21
- #include "facil.h"
22
-
23
- #include <stdio.h>
24
- #include <stdlib.h>
25
- #include <string.h>
26
-
27
- #ifndef UNUSED_FUNC
28
- #define UNUSED_FUNC __attribute__((unused))
29
- #endif
30
-
31
- extern VALUE Iodine;
32
- extern VALUE IodineBase;
33
-
34
- extern VALUE iodine_binary_var_id;
35
- extern VALUE iodine_channel_var_id;
36
- extern VALUE iodine_engine_var_id;
37
- extern VALUE iodine_force_var_id;
38
- extern VALUE iodine_message_var_id;
39
- extern VALUE iodine_pattern_var_id;
40
- extern VALUE iodine_text_var_id;
41
-
42
- extern ID iodine_buff_var_id;
43
- extern ID iodine_call_proc_id;
44
- extern ID iodine_cdata_var_id;
45
- extern ID iodine_fd_var_id;
46
- extern ID iodine_new_func_id;
47
- extern ID iodine_on_close_func_id;
48
- extern ID iodine_on_data_func_id;
49
- extern ID iodine_on_message_func_id;
50
- extern ID iodine_on_open_func_id;
51
- extern ID iodine_on_drained_func_id;
52
- extern ID iodine_on_shutdown_func_id;
53
- extern ID iodine_ping_func_id;
54
- extern ID iodine_timeout_var_id;
55
- extern ID iodine_to_i_func_id;
56
- extern ID iodine_to_s_method_id;
57
-
58
- extern rb_encoding *IodineBinaryEncoding;
59
- extern rb_encoding *IodineUTF8Encoding;
60
- extern int IodineBinaryEncodingIndex;
61
- extern int IodineUTF8EncodingIndex;
62
-
63
- UNUSED_FUNC static inline void iodine_set_fd(VALUE handler, intptr_t fd) {
64
- rb_ivar_set(handler, iodine_fd_var_id, LL2NUM((long)fd));
65
- }
66
- UNUSED_FUNC static inline intptr_t iodine_get_fd(VALUE handler) {
67
- return ((intptr_t)NUM2LL(rb_ivar_get(handler, iodine_fd_var_id)));
68
- }
69
-
70
- UNUSED_FUNC static inline void iodine_set_cdata(VALUE handler, void *protocol) {
71
- rb_ivar_set(handler, iodine_cdata_var_id, ULL2NUM((uintptr_t)protocol));
72
- }
73
- UNUSED_FUNC static inline void *iodine_get_cdata(VALUE handler) {
74
- return ((void *)NUM2ULL(rb_ivar_get(handler, iodine_cdata_var_id)));
75
- }
4
+ #include "ruby.h"
5
+
6
+ #include "iodine_caller.h"
7
+ #include "iodine_connection.h"
8
+ #include "iodine_defer.h"
9
+ #include "iodine_helpers.h"
10
+ #include "iodine_http.h"
11
+ #include "iodine_json.h"
12
+ #include "iodine_pubsub.h"
13
+ #include "iodine_rack_io.h"
14
+ #include "iodine_store.h"
15
+ #include "iodine_tcp.h"
16
+
17
+ /* *****************************************************************************
18
+ Constants
19
+ ***************************************************************************** */
20
+ extern VALUE IodineModule;
21
+ extern VALUE IodineBaseModule;
76
22
 
77
23
  #endif
@@ -0,0 +1,132 @@
1
+ #include "iodine_caller.h"
2
+
3
+ #include <ruby/thread.h>
4
+
5
+ static __thread volatile uint8_t iodine_GVL_state;
6
+
7
+ /* *****************************************************************************
8
+ Calling protected Ruby methods
9
+ ***************************************************************************** */
10
+
11
+ /* task container */
12
+ typedef struct {
13
+ VALUE obj;
14
+ int argc;
15
+ VALUE *argv;
16
+ ID method;
17
+ int exception;
18
+ } iodine_rb_task_s;
19
+
20
+ /* printout backtrace in case of exceptions */
21
+ static void *iodine_handle_exception(void *ignr) {
22
+ (void)ignr;
23
+ VALUE exc = rb_errinfo();
24
+ if (exc != Qnil) {
25
+ VALUE msg = rb_funcall2(exc, rb_intern("message"), 0, NULL);
26
+ VALUE exc_class = rb_class_name(CLASS_OF(exc));
27
+ VALUE bt = rb_funcall2(exc, rb_intern("backtrace"), 0, NULL);
28
+ if (TYPE(bt) == T_ARRAY) {
29
+ bt = rb_ary_join(bt, rb_str_new_literal("\n"));
30
+ fprintf(stderr, "Iodine caught an unprotected exception - %.*s: %.*s\n%s",
31
+ (int)RSTRING_LEN(exc_class), RSTRING_PTR(exc_class),
32
+ (int)RSTRING_LEN(msg), RSTRING_PTR(msg), StringValueCStr(bt));
33
+ } else {
34
+ fprintf(stderr,
35
+ "Iodine caught an unprotected exception - %.*s: %.*s\n"
36
+ "No backtrace available.\n",
37
+ (int)RSTRING_LEN(exc_class), RSTRING_PTR(exc_class),
38
+ (int)RSTRING_LEN(msg), RSTRING_PTR(msg));
39
+ }
40
+ rb_backtrace();
41
+ fprintf(stderr, "\n\n");
42
+ rb_set_errinfo(Qnil);
43
+ }
44
+ return (void *)Qnil;
45
+ }
46
+
47
+ /* calls the Ruby method within the protection block */
48
+ static VALUE iodine_ruby_caller_perform(VALUE tsk_) {
49
+ iodine_rb_task_s *task = (void *)tsk_;
50
+ return rb_funcall2(task->obj, task->method, task->argc, task->argv);
51
+ }
52
+
53
+ /* wrap the function call in exception handling block (uses longjmp) */
54
+ static void *iodine_protect_ruby_call(void *task_) {
55
+ int state = 0;
56
+ VALUE ret = rb_protect(iodine_ruby_caller_perform, (VALUE)(task_), &state);
57
+ if (state) {
58
+ iodine_handle_exception(NULL);
59
+ }
60
+ return (void *)ret;
61
+ }
62
+
63
+ /* *****************************************************************************
64
+ API
65
+ ***************************************************************************** */
66
+
67
+ /** Calls a C function within the GVL. */
68
+ static void *iodine_enterGVL(void *(*func)(void *), void *arg) {
69
+ if (iodine_GVL_state) {
70
+ return func(arg);
71
+ }
72
+ void *rv = NULL;
73
+ iodine_GVL_state = 1;
74
+ rv = rb_thread_call_with_gvl(func, arg);
75
+ iodine_GVL_state = 0;
76
+ return rv;
77
+ }
78
+
79
+ /** Calls a C function outside the GVL. */
80
+ static void *iodine_leaveGVL(void *(*func)(void *), void *arg) {
81
+ if (!iodine_GVL_state) {
82
+ return func(arg);
83
+ }
84
+ void *rv = NULL;
85
+ iodine_GVL_state = 0;
86
+ rv = rb_thread_call_without_gvl(func, arg, NULL, NULL);
87
+ iodine_GVL_state = 1;
88
+ return rv;
89
+ }
90
+
91
+ /** Calls a Ruby method on a given object, protecting against exceptions. */
92
+ static VALUE iodine_call(VALUE obj, ID method) {
93
+ iodine_rb_task_s task = {
94
+ .obj = obj, .argc = 0, .argv = NULL, .method = method,
95
+ };
96
+ void *rv = iodine_enterGVL(iodine_protect_ruby_call, &task);
97
+ return (VALUE)rv;
98
+ }
99
+
100
+ /** Calls a Ruby method on a given object, protecting against exceptions. */
101
+ static VALUE iodine_call2(VALUE obj, ID method, int argc, VALUE *argv) {
102
+ iodine_rb_task_s task = {
103
+ .obj = obj, .argc = argc, .argv = argv, .method = method,
104
+ };
105
+ void *rv = iodine_enterGVL(iodine_protect_ruby_call, &task);
106
+ return (VALUE)rv;
107
+ }
108
+
109
+ /** Returns the GVL state flag. */
110
+ static uint8_t iodine_in_GVL(void) { return iodine_GVL_state; }
111
+
112
+ /** Forces the GVL state flag. */
113
+ static void iodine_set_GVL(uint8_t state) { iodine_GVL_state = state; }
114
+
115
+ /* *****************************************************************************
116
+ Caller Initialization
117
+ ***************************************************************************** */
118
+
119
+ struct IodineCaller_s IodineCaller = {
120
+ /** Calls a C function within the GVL. */
121
+ .enterGVL = iodine_enterGVL,
122
+ /** Calls a C function outside the GVL. */
123
+ .leaveGVL = iodine_leaveGVL,
124
+ /** Calls a Ruby method on a given object, protecting against exceptions. */
125
+ .call = iodine_call,
126
+ /** Calls a Ruby method on a given object, protecting against exceptions. */
127
+ .call2 = iodine_call2,
128
+ /** Returns the GVL state flag. */
129
+ .in_GVL = iodine_in_GVL,
130
+ /** Forces the GVL state flag. */
131
+ .set_GVL = iodine_set_GVL,
132
+ };
@@ -0,0 +1,21 @@
1
+ #ifndef H_IODINE_CALLER_H
2
+ #define H_IODINE_CALLER_H
3
+
4
+ #include "ruby.h"
5
+
6
+ extern struct IodineCaller_s {
7
+ /** Calls a C function within the GVL (unprotected). */
8
+ void *(*enterGVL)(void *(*func)(void *), void *arg);
9
+ /** Calls a C function outside the GVL (no Ruby API calls allowed). */
10
+ void *(*leaveGVL)(void *(*func)(void *), void *arg);
11
+ /** Calls a Ruby method on a given object, protecting against exceptions. */
12
+ VALUE (*call)(VALUE obj, ID method);
13
+ /** Calls a Ruby method on a given object, protecting against exceptions. */
14
+ VALUE (*call2)(VALUE obj, ID method, int argc, VALUE *argv);
15
+ /** Returns the GVL state flag. */
16
+ uint8_t (*in_GVL)(void);
17
+ /** Forces the GVL state flag. */
18
+ void (*set_GVL)(uint8_t state);
19
+ } IodineCaller;
20
+
21
+ #endif
@@ -0,0 +1,841 @@
1
+ #include "iodine_connection.h"
2
+
3
+ #include "facil.h"
4
+ #include "fio_mem.h"
5
+ #include "fiobj4sock.h"
6
+ #include "pubsub.h"
7
+ #include "websockets.h"
8
+
9
+ #include "spnlock.inc"
10
+
11
+ #include <ruby/io.h>
12
+
13
+ /* *****************************************************************************
14
+ Constants in use
15
+ ***************************************************************************** */
16
+
17
+ static ID new_id;
18
+ static ID call_id;
19
+ static ID to_id;
20
+ static ID channel_id;
21
+ static ID as_id;
22
+ static ID binary_id;
23
+ static ID match_id;
24
+ static ID redis_id;
25
+ static ID handler_id;
26
+ static ID engine_id;
27
+ static ID message_id;
28
+ static ID on_open_id;
29
+ static ID on_message_id;
30
+ static ID on_drained_id;
31
+ static ID ping_id;
32
+ static ID on_shutdown_id;
33
+ static ID on_close_id;
34
+ static VALUE ConnectionKlass;
35
+ static rb_encoding *IodineUTF8Encoding;
36
+ static VALUE WebSocketSymbol;
37
+ static VALUE SSESymbol;
38
+ static VALUE RAWSymbol;
39
+
40
+ /* *****************************************************************************
41
+ Pub/Sub storage
42
+ ***************************************************************************** */
43
+
44
+ #define FIO_HASH_KEY_TYPE FIOBJ
45
+ #define FIO_HASH_KEY_INVALID FIOBJ_INVALID
46
+ #define FIO_HASH_KEY2UINT(key) fiobj_obj2hash((key))
47
+ #define FIO_HASH_COMPARE_KEYS(k1, k2) (fiobj_iseq((k1), (k2)))
48
+ #define FIO_HASH_KEY_ISINVALID(key) ((key) == FIOBJ_INVALID)
49
+ #define FIO_HASH_KEY_COPY(key) (fiobj_dup(key))
50
+ #define FIO_HASH_KEY_DESTROY(key) (fiobj_free((key)))
51
+
52
+ #include "fio_hashmap.h"
53
+
54
+ static inline VALUE iodine_sub_unsubscribe(fio_hash_s *store, FIOBJ channel) {
55
+ pubsub_sub_pt sub = fio_hash_insert(store, channel, NULL);
56
+ if (sub) {
57
+ pubsub_unsubscribe(sub);
58
+ return Qtrue;
59
+ }
60
+ return Qfalse;
61
+ }
62
+ static inline void iodine_sub_add(fio_hash_s *store, pubsub_sub_pt sub) {
63
+ FIOBJ channel = pubsub_sub_channel(sub); /* used for memory optimization */
64
+ sub = fio_hash_insert(store, channel, sub);
65
+ if (sub) {
66
+ pubsub_unsubscribe(sub);
67
+ }
68
+ }
69
+ static inline void iodine_sub_clear_all(fio_hash_s *store) {
70
+ FIO_HASH_FOR_FREE(store, pos) {
71
+ if (pos->obj) {
72
+ pubsub_unsubscribe(pos->obj);
73
+ }
74
+ }
75
+ }
76
+
77
+ static spn_lock_i sub_lock = SPN_LOCK_INIT;
78
+ static fio_hash_s sub_global = FIO_HASH_INIT;
79
+
80
+ /* *****************************************************************************
81
+ C <=> Ruby Data allocation
82
+ ***************************************************************************** */
83
+
84
+ typedef struct {
85
+ iodine_connection_s info;
86
+ size_t ref;
87
+ fio_hash_s subscriptions;
88
+ spn_lock_i lock;
89
+ uint8_t answers_on_message;
90
+ uint8_t answers_on_drained;
91
+ uint8_t answers_ping;
92
+ /* these are one-shot, but the CPU cache might have the data, so set it */
93
+ uint8_t answers_on_open;
94
+ uint8_t answers_on_shutdown;
95
+ uint8_t answers_on_close;
96
+ } iodine_connection_data_s;
97
+
98
+ /** frees an iodine_connection_data_s object*/
99
+
100
+ /* a callback for the GC (marking active objects) */
101
+ static void iodine_connection_data_mark(void *c_) {
102
+ iodine_connection_data_s *c = c_;
103
+ if (c->info.handler != Qnil) {
104
+ rb_gc_mark(c->info.handler);
105
+ }
106
+ if (c->info.env && c->info.env != Qnil) {
107
+ rb_gc_mark(c->info.env);
108
+ }
109
+ }
110
+ /* a callback for the GC (marking active objects) */
111
+ static void iodine_connection_data_free(void *c_) {
112
+ iodine_connection_data_s *data = c_;
113
+ if (spn_sub(&data->ref, 1))
114
+ return;
115
+ free(data);
116
+ }
117
+
118
+ static size_t iodine_connection_data_size(const void *c_) {
119
+ return sizeof(iodine_connection_data_s);
120
+ (void)c_;
121
+ }
122
+
123
+ const rb_data_type_t iodine_connection_data_type = {
124
+ .wrap_struct_name = "IodineConnectionData",
125
+ .function =
126
+ {
127
+ .dmark = iodine_connection_data_mark,
128
+ .dfree = iodine_connection_data_free,
129
+ .dsize = iodine_connection_data_size,
130
+ },
131
+ .data = NULL,
132
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
133
+ };
134
+
135
+ /* Iodine::PubSub::Engine.allocate */
136
+ static VALUE iodine_connection_data_alloc_c(VALUE self) {
137
+ iodine_connection_data_s *c = malloc(sizeof(*c));
138
+ *c = (iodine_connection_data_s){
139
+ .info.handler = (VALUE)0,
140
+ .info.uuid = -1,
141
+ .ref = 1,
142
+ .subscriptions = FIO_HASH_INIT,
143
+ .lock = SPN_LOCK_INIT,
144
+ };
145
+ return TypedData_Wrap_Struct(self, &iodine_connection_data_type, c);
146
+ }
147
+
148
+ static inline iodine_connection_data_s *iodine_connection_ruby2C(VALUE self) {
149
+ iodine_connection_data_s *c = NULL;
150
+ TypedData_Get_Struct(self, iodine_connection_data_s,
151
+ &iodine_connection_data_type, c);
152
+ return c;
153
+ }
154
+
155
+ static inline iodine_connection_data_s *
156
+ iodine_connection_validate_data(VALUE self) {
157
+ iodine_connection_data_s *c = iodine_connection_ruby2C(self);
158
+ if (c == NULL || c->info.handler == Qnil || c->info.uuid == -1) {
159
+ return NULL;
160
+ }
161
+ return c;
162
+ }
163
+
164
+ /* *****************************************************************************
165
+ Ruby Connection Methods - write, close open? pending
166
+ ***************************************************************************** */
167
+
168
+ /**
169
+ * Writes data to the connection asynchronously.
170
+ *
171
+ * In effect, the `write` call does nothing, it only schedules the data to be
172
+ * sent and marks the data as pending.
173
+ *
174
+ * Use {pending} to test how many `write` operations are pending completion
175
+ * (`on_drained(client)` will be called when they complete).
176
+ */
177
+ static VALUE iodine_connection_write(VALUE self, VALUE data) {
178
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
179
+ if (!c || sock_isclosed(c->info.uuid)) {
180
+ rb_raise(rb_eIOError, "Connection closed or invalid.");
181
+ }
182
+ switch (c->info.type) {
183
+ case IODINE_CONNECTION_WEBSOCKET:
184
+ /* WebSockets*/
185
+ websocket_write(c->info.arg, RSTRING_PTR(data), RSTRING_LEN(data),
186
+ rb_enc_get(data) == IodineUTF8Encoding);
187
+ return Qtrue;
188
+ break;
189
+ case IODINE_CONNECTION_SSE:
190
+ /* SSE */
191
+ #if 1
192
+ http_sse_write(c->info.arg, .data = {.data = RSTRING_PTR(data),
193
+ .len = RSTRING_LEN(data)});
194
+ return Qtrue;
195
+ #else
196
+ if (rb_enc_get(data) == IodineUTF8Encoding) {
197
+ http_sse_write(c->info.arg, .data = {.data = RSTRING_PTR(data),
198
+ .len = RSTRING_LEN(data)});
199
+ return Qtrue;
200
+ }
201
+ fprintf(stderr, "WARNING: ignoring an attept to write binary data to "
202
+ "non-binary protocol (SSE).\n");
203
+ return Qfalse;
204
+ // rb_raise(rb_eEncodingError,
205
+ // "This Connection type requires data to be UTF-8 encoded.");
206
+ #endif
207
+ break;
208
+ case IODINE_CONNECTION_RAW: /* fallthrough */
209
+ default: {
210
+ size_t len = RSTRING_LEN(data);
211
+ char *copy = fio_malloc(len);
212
+ if (!copy) {
213
+ rb_raise(rb_eNoMemError, "failed to allocate memory for network buffer!");
214
+ }
215
+ memcpy(copy, RSTRING_PTR(data), len);
216
+ sock_write2(.uuid = c->info.uuid, .buffer = copy, .length = len,
217
+ .dealloc = fio_free);
218
+ return Qtrue;
219
+ } break;
220
+ }
221
+ return Qnil;
222
+ }
223
+
224
+ /**
225
+ * Schedules the connection to be closed.
226
+ *
227
+ * The connection will be closed once all the scheduled `write` operations have
228
+ * been completed (or failed).
229
+ */
230
+ static VALUE iodine_connection_close(VALUE self) {
231
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
232
+ if (c && !sock_isclosed(c->info.uuid)) {
233
+ if (c->info.type == IODINE_CONNECTION_WEBSOCKET) {
234
+ websocket_close(c->info.arg);
235
+ } else {
236
+ sock_close(c->info.uuid);
237
+ }
238
+ }
239
+
240
+ return Qnil;
241
+ }
242
+ /** Returns true if the connection appears to be open (no known issues). */
243
+ static VALUE iodine_connection_is_open(VALUE self) {
244
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
245
+ if (c && !sock_isclosed(c->info.uuid)) {
246
+ return Qtrue;
247
+ }
248
+ return Qfalse;
249
+ }
250
+ /**
251
+ * Returns the number of pending `write` operations that need to complete
252
+ * before the next `on_drained` callback is called.
253
+ */
254
+ static VALUE iodine_connection_pending(VALUE self) {
255
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
256
+ if (!c || sock_isclosed(c->info.uuid)) {
257
+ return INT2NUM(-1);
258
+ }
259
+ return SIZET2NUM((sock_pending(c->info.uuid)));
260
+ }
261
+
262
+ /** Returns the connection's protocol Symbol (`:sse`, `:websocket`, etc'). */
263
+ static VALUE iodine_connection_protocol_name(VALUE self) {
264
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
265
+ if (c) {
266
+ switch (c->info.type) {
267
+ case IODINE_CONNECTION_WEBSOCKET:
268
+ return WebSocketSymbol;
269
+ break;
270
+ case IODINE_CONNECTION_SSE:
271
+ return SSESymbol;
272
+ break;
273
+ case IODINE_CONNECTION_RAW: /* fallthrough */
274
+ return RAWSymbol;
275
+ break;
276
+ }
277
+ }
278
+ return Qnil;
279
+ }
280
+
281
+ /**
282
+ * Returns the timeout / `ping` interval for the connection.
283
+ *
284
+ * Returns nil on error.
285
+ */
286
+ static VALUE iodine_connection_timeout_get(VALUE self) {
287
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
288
+ if (c && !sock_isclosed(c->info.uuid)) {
289
+ size_t tout = (size_t)facil_get_timeout(c->info.uuid);
290
+ return SIZET2NUM(tout);
291
+ }
292
+ return Qnil;
293
+ }
294
+
295
+ /**
296
+ * Sets the timeout / `ping` interval for the connection (up to 255 seconds).
297
+ *
298
+ * Returns nil on error.
299
+ */
300
+ static VALUE iodine_connection_timeout_set(VALUE self, VALUE timeout) {
301
+ Check_Type(timeout, T_FIXNUM);
302
+ int tout = NUM2INT(timeout);
303
+ if (tout < 0 || tout > 255) {
304
+ rb_raise(rb_eRangeError, "timeout out of range.");
305
+ return Qnil;
306
+ }
307
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
308
+ if (c && !sock_isclosed(c->info.uuid)) {
309
+ facil_set_timeout(c->info.uuid, (uint8_t)tout);
310
+ return timeout;
311
+ }
312
+ return Qnil;
313
+ }
314
+
315
+ /**
316
+ * Returns the connection's `env` (if it originated from an HTTP request).
317
+ */
318
+ static VALUE iodine_connection_env(VALUE self) {
319
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
320
+ if (c && c->info.env) {
321
+ return c->info.env;
322
+ }
323
+ return Qnil;
324
+ }
325
+
326
+ /* *****************************************************************************
327
+ Pub/Sub Callbacks (internal implementation)
328
+ ***************************************************************************** */
329
+
330
+ /* calls the Ruby block assigned to a pubsub event (within the GVL). */
331
+ static void *iodine_on_pubsub_call_block(void *msg_) {
332
+ pubsub_message_s *msg = msg_;
333
+ fio_cstr_s tmp;
334
+ VALUE args[2];
335
+ tmp = fiobj_obj2cstr(msg->channel);
336
+ args[0] = rb_str_new(tmp.data, tmp.len);
337
+ tmp = fiobj_obj2cstr(msg->message);
338
+ args[1] = rb_str_new(tmp.data, tmp.len);
339
+ IodineCaller.call2((VALUE)msg->udata2, call_id, 2, args);
340
+ return NULL;
341
+ }
342
+
343
+ /* callback for incoming subscription messages */
344
+ static void iodine_on_pubsub(pubsub_message_s *msg) {
345
+ iodine_connection_data_s *data = msg->udata1;
346
+ VALUE block = (VALUE)msg->udata2;
347
+ switch (block) {
348
+ case Qnil: /* fallthrough */
349
+ case Qtrue: {
350
+ if (data->info.handler == Qnil || data->info.uuid == -1 ||
351
+ sock_isclosed(data->info.uuid))
352
+ return;
353
+ switch (data->info.type) {
354
+ case IODINE_CONNECTION_WEBSOCKET: {
355
+ fio_cstr_s str = fiobj_obj2cstr(msg->message);
356
+ websocket_write(data->info.arg, str.data, str.len, (block == Qnil));
357
+ return;
358
+ }
359
+ case IODINE_CONNECTION_SSE:
360
+ http_sse_write(data->info.arg, .data = fiobj_obj2cstr(msg->message));
361
+ return;
362
+ default:
363
+ fiobj_send_free(data->info.uuid, fiobj_dup(msg->message));
364
+ return;
365
+ }
366
+ }
367
+ default:
368
+ IodineCaller.enterGVL(iodine_on_pubsub_call_block, msg);
369
+ break;
370
+ }
371
+ }
372
+
373
+ /* callback for subscription closure */
374
+ static void iodine_on_unsubscribe(void *udata1, void *udata2) {
375
+ iodine_connection_data_s *data = udata1;
376
+ VALUE block = (VALUE)udata2;
377
+ switch (block) {
378
+ case Qnil:
379
+ case Qtrue:
380
+ break;
381
+ default:
382
+ IodineStore.remove(block);
383
+ break;
384
+ }
385
+ if (data) {
386
+ iodine_connection_data_free(data);
387
+ }
388
+ }
389
+
390
+ /* *****************************************************************************
391
+ Ruby Connection Methods - Pub/Sub
392
+ ***************************************************************************** */
393
+
394
+ typedef struct {
395
+ VALUE channel;
396
+ VALUE block;
397
+ uint8_t binary;
398
+ uint8_t pattern;
399
+ } iodine_sub_args_s;
400
+
401
+ /** Tests the `subscribe` Ruby arguments */
402
+ static iodine_sub_args_s iodine_subscribe_args(int argc, VALUE *argv) {
403
+
404
+ iodine_sub_args_s ret = {.channel = Qnil, .block = Qnil};
405
+ VALUE rb_opt = 0;
406
+
407
+ switch (argc) {
408
+ case 2:
409
+ ret.channel = argv[0];
410
+ rb_opt = argv[1];
411
+ break;
412
+ case 1:
413
+ /* single argument must be a Hash / channel name */
414
+ if (TYPE(argv[0]) == T_HASH) {
415
+ rb_opt = argv[0];
416
+ ret.channel = rb_hash_aref(argv[0], to_id);
417
+ if (ret.channel == Qnil || ret.channel == Qfalse) {
418
+ /* temporary backport support */
419
+ ret.channel = rb_hash_aref(argv[0], channel_id);
420
+ if (ret.channel != Qnil) {
421
+ fprintf(stderr,
422
+ "WARNING: use of :channel in subscribe is deprecated.\n");
423
+ }
424
+ }
425
+ } else {
426
+ ret.channel = argv[0];
427
+ }
428
+ break;
429
+ default:
430
+ rb_raise(rb_eArgError, "method accepts 1 or 2 arguments.");
431
+ return ret;
432
+ }
433
+
434
+ if (ret.channel == Qnil || ret.channel == Qfalse) {
435
+ rb_raise(rb_eArgError,
436
+ "a target (:to) subject / stream / channel is required.");
437
+ return ret;
438
+ }
439
+
440
+ if (TYPE(ret.channel) == T_SYMBOL)
441
+ ret.channel = rb_sym2str(ret.channel);
442
+ Check_Type(ret.channel, T_STRING);
443
+
444
+ if (rb_opt) {
445
+ if (rb_hash_aref(rb_opt, as_id) == binary_id) {
446
+ ret.binary = 1;
447
+ }
448
+ if (rb_hash_aref(rb_opt, match_id) == redis_id) {
449
+ ret.pattern = 1;
450
+ }
451
+ ret.block = rb_hash_aref(rb_opt, handler_id);
452
+ if (ret.block != Qnil) {
453
+ IodineStore.add(ret.block);
454
+ }
455
+ }
456
+
457
+ if (ret.block == Qnil) {
458
+ if (rb_block_given_p()) {
459
+ ret.block = rb_block_proc();
460
+ IodineStore.add(ret.block);
461
+ }
462
+ }
463
+ return ret;
464
+ }
465
+
466
+ // clang-format off
467
+ /**
468
+ Subscribes to a Pub/Sub stream / channel or replaces an existing subscription.
469
+
470
+ The method accepts 1-2 arguments and an optional block. These are all valid ways
471
+ to call the method:
472
+
473
+ subscribe("my_stream") {|source, msg| p msg }
474
+ subscribe("my_strea*", match: :redis) {|source, msg| p msg }
475
+ subscribe(to: "my_stream") {|source, msg| p msg }
476
+ # or use any object that answers `#call(source, msg)`
477
+ MyProc = Proc.new {|source, msg| p msg }
478
+ subscribe to: "my_stream", match: :redis, handler: MyProc
479
+
480
+ The first argument must be either a String or a Hash.
481
+
482
+ The second, optional, argument must be a Hash (if given).
483
+
484
+ The options Hash supports the following possible keys (other keys are ignored, all keys are Symbols):
485
+
486
+ :match :: The channel / subject name matching type to be used. Valid value is: `:redis`. Future versions hope to support `:nats` and `:rabbit` patern matching as well.
487
+
488
+ :to :: The channel / subject to subscribe to.
489
+
490
+ :as :: (only for WebSocket connections) accepts the optional value `:binary`. default is `:text`. Note that binary transmissions are illegal for some connections (such as SSE) and an attempted binary subscription will fail for these connections.
491
+
492
+ :handler :: Any object that answers `#call(source, msg)` where source is the stream / channel name.
493
+
494
+ Note: if an existing subscription with the same name exists, it will be replaced by this new subscription.
495
+
496
+ Returns the name of the subscription, which matches the name be used in {unsubscribe} (or nil on failure).
497
+
498
+ */
499
+ static VALUE iodine_pubsub_subscribe(int argc, VALUE *argv, VALUE self) {
500
+ // clang-format on
501
+ iodine_sub_args_s args = iodine_subscribe_args(argc, argv);
502
+ if (args.channel == Qnil) {
503
+ return Qnil;
504
+ }
505
+ iodine_connection_data_s *c = NULL;
506
+ if (TYPE(self) == T_MODULE) {
507
+ if (!args.block) {
508
+ rb_raise(rb_eArgError,
509
+ "block or :handler required for local subscriptions.");
510
+ }
511
+ } else {
512
+ c = iodine_connection_validate_data(self);
513
+ if (!c || (c->info.type == IODINE_CONNECTION_SSE && args.binary)) {
514
+ if (args.block) {
515
+ IodineStore.remove(args.block);
516
+ }
517
+ return Qnil; /* cannot subscribe a closed connection. */
518
+ }
519
+ if (args.block == Qnil && args.binary) {
520
+ args.block = Qtrue;
521
+ }
522
+ spn_add(&c->ref, 1);
523
+ }
524
+
525
+ FIOBJ channel =
526
+ fiobj_str_new(RSTRING_PTR(args.channel), RSTRING_LEN(args.channel));
527
+ pubsub_sub_pt sub =
528
+ pubsub_subscribe(.channel = channel, .on_message = iodine_on_pubsub,
529
+ .on_unsubscribe = iodine_on_unsubscribe, .udata1 = c,
530
+ .udata2 = (void *)args.block,
531
+ .use_pattern = args.pattern);
532
+ fiobj_free(channel);
533
+ if (c) {
534
+ spn_lock(&c->lock);
535
+ if (c->info.uuid == -1) {
536
+ pubsub_unsubscribe(sub);
537
+ spn_unlock(&c->lock);
538
+ return Qnil;
539
+ } else {
540
+ iodine_sub_add(&c->subscriptions, sub);
541
+ }
542
+ spn_unlock(&c->lock);
543
+ } else {
544
+ spn_lock(&sub_lock);
545
+ iodine_sub_add(&sub_global, sub);
546
+ spn_unlock(&sub_lock);
547
+ }
548
+ return args.channel;
549
+ }
550
+
551
+ // clang-format off
552
+ /**
553
+ Unsubscribes from a Pub/Sub stream / channel.
554
+
555
+ The method accepts a single arguments, the name used for the subscription. i.e.:
556
+
557
+ subscribe("my_stream") {|source, msg| p msg }
558
+ unsubscribe("my_stream")
559
+
560
+ Returns `true` if the subscription was found.
561
+
562
+ Returns `false` if the subscription didn't exist.
563
+ */
564
+ static VALUE iodine_pubsub_unsubscribe(VALUE self, VALUE name) {
565
+ // clang-format on
566
+ iodine_connection_data_s *c = NULL;
567
+ FIOBJ channel = fiobj_str_new(RSTRING_PTR(name), RSTRING_LEN(name));
568
+ VALUE ret;
569
+ if (TYPE(self) == T_MODULE) {
570
+ spn_lock(&sub_lock);
571
+ ret = iodine_sub_unsubscribe(&sub_global, channel);
572
+ spn_unlock(&sub_lock);
573
+ } else {
574
+ c = iodine_connection_validate_data(self);
575
+ if (!c) {
576
+ return Qnil; /* cannot subscribe a closed connection. */
577
+ }
578
+ spn_lock(&sub_lock);
579
+ ret = iodine_sub_unsubscribe(&sub_global, channel);
580
+ spn_unlock(&sub_lock);
581
+ }
582
+ fiobj_free(channel);
583
+ return ret;
584
+ }
585
+
586
+ /**
587
+ Publishes a message to a channel.
588
+
589
+ Can be used using two Strings:
590
+
591
+ publish(to, message)
592
+
593
+ The method accepts an optional `engine` argument:
594
+
595
+ publish(to, message, my_pubsub_engine)
596
+
597
+
598
+ Alternatively, accepts the following named arguments:
599
+
600
+ :to :: The channel to publish to (required).
601
+
602
+ :message :: The message to be published (required).
603
+
604
+ :engine :: If provided, the engine to use for pub/sub. Otherwise the default
605
+ engine is used.
606
+
607
+ */
608
+ static VALUE iodine_pubsub_publish(int argc, VALUE *argv, VALUE self) {
609
+ VALUE rb_ch, rb_msg, rb_engine = Qnil;
610
+ const pubsub_engine_s *engine = NULL;
611
+ switch (argc) {
612
+ case 3:
613
+ /* fallthrough */
614
+ rb_engine = argv[2];
615
+ case 2:
616
+ rb_ch = argv[0];
617
+ rb_msg = argv[1];
618
+ break;
619
+ case 1: {
620
+ /* single argument must be a Hash */
621
+ Check_Type(argv[0], T_HASH);
622
+ rb_ch = rb_hash_aref(argv[0], to_id);
623
+ if (rb_ch == Qnil || rb_ch == Qfalse) {
624
+ rb_ch = rb_hash_aref(argv[0], channel_id);
625
+ }
626
+ rb_msg = rb_hash_aref(argv[0], message_id);
627
+ rb_engine = rb_hash_aref(argv[0], engine_id);
628
+ } break;
629
+ default:
630
+ rb_raise(rb_eArgError, "method accepts 1-3 arguments.");
631
+ }
632
+
633
+ if (rb_msg == Qnil || rb_msg == Qfalse) {
634
+ rb_raise(rb_eArgError, "message is required.");
635
+ }
636
+ Check_Type(rb_msg, T_STRING);
637
+
638
+ if (rb_ch == Qnil || rb_ch == Qfalse)
639
+ rb_raise(rb_eArgError, "target / channel is required .");
640
+ if (TYPE(rb_ch) == T_SYMBOL)
641
+ rb_ch = rb_sym2str(rb_ch);
642
+ Check_Type(rb_ch, T_STRING);
643
+
644
+ if (rb_engine == Qfalse) {
645
+ engine = PUBSUB_PROCESS_ENGINE;
646
+ } else if (rb_engine != Qnil) {
647
+ // collect engine object
648
+ iodine_pubsub_s *e = iodine_pubsub_CData(rb_engine);
649
+ if (e) {
650
+ engine = e->engine;
651
+ }
652
+ }
653
+
654
+ FIOBJ ch = fiobj_str_new(RSTRING_PTR(rb_ch), RSTRING_LEN(rb_ch));
655
+ FIOBJ msg = fiobj_str_new(RSTRING_PTR(rb_msg), RSTRING_LEN(rb_msg));
656
+
657
+ intptr_t ret =
658
+ pubsub_publish(.engine = engine, .channel = ch, .message = msg);
659
+ fiobj_free(ch);
660
+ fiobj_free(msg);
661
+ if (!ret)
662
+ return Qfalse;
663
+ return Qtrue;
664
+ (void)self;
665
+ }
666
+
667
+ /* *****************************************************************************
668
+ Published C functions
669
+ ***************************************************************************** */
670
+
671
+ #undef iodine_connection_new
672
+ VALUE iodine_connection_new(iodine_connection_s args) {
673
+ VALUE connection = IodineCaller.call(ConnectionKlass, new_id);
674
+ if (connection == Qnil) {
675
+ return Qnil;
676
+ }
677
+ IodineStore.add(connection);
678
+ iodine_connection_data_s *data = iodine_connection_ruby2C(connection);
679
+ if (data == NULL) {
680
+ fprintf(
681
+ stderr,
682
+ "ERROR: (iodine) internal error, connection object has no C data!\n");
683
+ return Qnil;
684
+ }
685
+ *data = (iodine_connection_data_s){
686
+ .info = args,
687
+ .subscriptions = FIO_HASH_INIT,
688
+ .ref = 1,
689
+ .answers_on_open = (rb_respond_to(args.handler, on_open_id) != 0),
690
+ .answers_on_message = (rb_respond_to(args.handler, on_message_id) != 0),
691
+ .answers_ping = (rb_respond_to(args.handler, ping_id) != 0),
692
+ .answers_on_drained = (rb_respond_to(args.handler, on_drained_id) != 0),
693
+ .answers_on_shutdown = (rb_respond_to(args.handler, on_shutdown_id) != 0),
694
+ .answers_on_close = (rb_respond_to(args.handler, on_close_id) != 0),
695
+ .lock = SPN_LOCK_INIT,
696
+ };
697
+ return connection;
698
+ }
699
+
700
+ /** Fires a connection object's event */
701
+ void iodine_connection_fire_event(VALUE connection,
702
+ iodine_connection_event_type_e ev,
703
+ VALUE msg) {
704
+ if (connection == Qnil) {
705
+ fprintf(
706
+ stderr,
707
+ "ERROR: (iodine) nil connection handle used by an internal API call\n");
708
+ return;
709
+ }
710
+ iodine_connection_data_s *data = iodine_connection_validate_data(connection);
711
+ if (!data) {
712
+ fprintf(stderr,
713
+ "ERROR: (iodine) invalid connection handle used by an "
714
+ "internal API call: %p\n",
715
+ (void *)connection);
716
+ return;
717
+ }
718
+ VALUE args[2] = {connection, msg};
719
+ switch (ev) {
720
+ case IODINE_CONNECTION_ON_OPEN:
721
+ if (data->answers_on_open) {
722
+ IodineCaller.call2(data->info.handler, on_open_id, 1, args);
723
+ }
724
+ break;
725
+ case IODINE_CONNECTION_ON_MESSAGE:
726
+ if (data->answers_on_message) {
727
+ IodineCaller.call2(data->info.handler, on_message_id, 2, args);
728
+ }
729
+ break;
730
+ case IODINE_CONNECTION_ON_DRAINED:
731
+ if (data->answers_on_drained) {
732
+ IodineCaller.call2(data->info.handler, on_drained_id, 1, args);
733
+ }
734
+ break;
735
+ case IODINE_CONNECTION_ON_SHUTDOWN:
736
+ if (data->answers_on_shutdown) {
737
+ IodineCaller.call2(data->info.handler, on_shutdown_id, 1, args);
738
+ }
739
+ break;
740
+ case IODINE_CONNECTION_PING:
741
+ if (data->answers_ping) {
742
+ IodineCaller.call2(data->info.handler, ping_id, 1, args);
743
+ }
744
+ break;
745
+
746
+ case IODINE_CONNECTION_ON_CLOSE:
747
+ if (data->answers_on_close) {
748
+ IodineCaller.call2(data->info.handler, on_close_id, 1, args);
749
+ }
750
+ spn_lock(&data->lock);
751
+ data->info.handler = Qnil;
752
+ data->info.env = Qnil;
753
+ data->info.uuid = -1;
754
+ data->info.arg = NULL;
755
+ iodine_sub_clear_all(&data->subscriptions);
756
+ spn_unlock(&data->lock);
757
+ IodineStore.remove(connection);
758
+ break;
759
+ default:
760
+ break;
761
+ }
762
+ }
763
+
764
+ void iodine_connection_init(void) {
765
+ // set used constants
766
+ IodineUTF8Encoding = rb_enc_find("UTF-8");
767
+ // used ID objects
768
+ new_id = rb_intern2("new", 3);
769
+ call_id = rb_intern2("call", 4);
770
+
771
+ to_id = rb_intern2("to", 2);
772
+ channel_id = rb_intern2("channel", 7);
773
+ as_id = rb_intern2("as", 2);
774
+ binary_id = rb_intern2("binary", 6);
775
+ match_id = rb_intern2("match", 5);
776
+ redis_id = rb_intern2("redis", 5);
777
+ handler_id = rb_intern2("handler", 7);
778
+ engine_id = rb_intern2("engine", 6);
779
+ message_id = rb_intern2("message", 7);
780
+ on_open_id = rb_intern("on_open");
781
+ on_message_id = rb_intern("on_message");
782
+ on_drained_id = rb_intern("on_drained");
783
+ on_shutdown_id = rb_intern("on_shutdown");
784
+ on_close_id = rb_intern("on_close");
785
+ ping_id = rb_intern("ping");
786
+
787
+ // globalize ID objects
788
+ if (1) {
789
+ IodineStore.add(ID2SYM(to_id));
790
+ IodineStore.add(ID2SYM(channel_id));
791
+ IodineStore.add(ID2SYM(as_id));
792
+ IodineStore.add(ID2SYM(binary_id));
793
+ IodineStore.add(ID2SYM(match_id));
794
+ IodineStore.add(ID2SYM(redis_id));
795
+ IodineStore.add(ID2SYM(handler_id));
796
+ IodineStore.add(ID2SYM(engine_id));
797
+ IodineStore.add(ID2SYM(message_id));
798
+ IodineStore.add(ID2SYM(on_open_id));
799
+ IodineStore.add(ID2SYM(on_message_id));
800
+ IodineStore.add(ID2SYM(on_drained_id));
801
+ IodineStore.add(ID2SYM(on_shutdown_id));
802
+ IodineStore.add(ID2SYM(on_close_id));
803
+ IodineStore.add(ID2SYM(ping_id));
804
+ }
805
+
806
+ // should these be globalized?
807
+ WebSocketSymbol = ID2SYM(rb_intern("websocket"));
808
+ SSESymbol = ID2SYM(rb_intern("sse"));
809
+ RAWSymbol = ID2SYM(rb_intern("raw"));
810
+ IodineStore.add(WebSocketSymbol);
811
+ IodineStore.add(SSESymbol);
812
+ IodineStore.add(RAWSymbol);
813
+
814
+ // define the Connection Class and it's methods
815
+ ConnectionKlass =
816
+ rb_define_class_under(IodineModule, "Connection", rb_cObject);
817
+ rb_define_alloc_func(ConnectionKlass, iodine_connection_data_alloc_c);
818
+ rb_define_method(ConnectionKlass, "write", iodine_connection_write, 1);
819
+ rb_define_method(ConnectionKlass, "close", iodine_connection_close, 0);
820
+ rb_define_method(ConnectionKlass, "open?", iodine_connection_is_open, 0);
821
+ rb_define_method(ConnectionKlass, "pending", iodine_connection_pending, 0);
822
+ rb_define_method(ConnectionKlass, "protocol", iodine_connection_protocol_name,
823
+ 0);
824
+ rb_define_method(ConnectionKlass, "timeout", iodine_connection_timeout_get,
825
+ 0);
826
+ rb_define_method(ConnectionKlass, "timeout=", iodine_connection_timeout_set,
827
+ 1);
828
+ rb_define_method(ConnectionKlass, "env", iodine_connection_env, 0);
829
+
830
+ rb_define_method(ConnectionKlass, "subscribe", iodine_pubsub_subscribe, -1);
831
+ rb_define_method(ConnectionKlass, "unsubscribe", iodine_pubsub_unsubscribe,
832
+ 1);
833
+ rb_define_method(ConnectionKlass, "publish", iodine_pubsub_publish, -1);
834
+
835
+ // define global methods
836
+ rb_define_module_function(IodineModule, "subscribe", iodine_pubsub_subscribe,
837
+ -1);
838
+ rb_define_module_function(IodineModule, "unsubscribe",
839
+ iodine_pubsub_unsubscribe, 1);
840
+ rb_define_module_function(IodineModule, "publish", iodine_pubsub_publish, -1);
841
+ }