tests: Run just built nbdkit, not installed nbdkit.
[nbdkit/ericb.git] / plugins / ruby / ruby.c
blobff7932c403a391ce593d1e4079d5e5cf004a11d3
1 /* nbdkit
2 * Copyright (C) 2013-2018 Red Hat Inc.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
15 * * Neither the name of Red Hat nor the names of its contributors may be
16 * used to endorse or promote products derived from this software without
17 * specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
29 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
33 #include <config.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <assert.h>
38 #include <errno.h>
40 #include <nbdkit-plugin.h>
42 #include <ruby.h>
43 #ifdef HAVE_RUBY_VERSION_H
44 #include <ruby/version.h>
45 #endif
47 static VALUE nbdkit_module = Qnil;
48 static int last_error;
50 static VALUE
51 set_error (VALUE self, VALUE arg)
53 int err;
54 VALUE v;
56 if (TYPE(arg) == T_CLASS) {
57 v = rb_const_get (arg, rb_intern ("Errno"));
58 err = NUM2INT (v);
59 } else if (TYPE (arg) == T_OBJECT) {
60 v = rb_funcall (arg, rb_intern ("errno"), 0);
61 err = NUM2INT (v);
62 } else {
63 err = NUM2INT (arg);
65 last_error = err;
66 nbdkit_set_error (err);
67 return Qnil;
70 static void
71 plugin_rb_load (void)
73 RUBY_INIT_STACK;
74 ruby_init ();
75 ruby_init_loadpath ();
77 nbdkit_module = rb_define_module ("Nbdkit");
78 rb_define_module_function (nbdkit_module, "set_error", set_error, 1);
81 /* https://stackoverflow.com/questions/11086549/how-to-rb-protect-everything-in-ruby */
82 #define MAX_ARGS 16
83 struct callback_data {
84 VALUE receiver; /* object being called */
85 ID method_id; /* method on object being called */
86 int argc; /* number of args */
87 VALUE argv[MAX_ARGS]; /* list of args */
90 static VALUE
91 callback_dispatch (VALUE datav)
93 struct callback_data *data = (struct callback_data *) datav;
94 return rb_funcall2 (data->receiver, data->method_id, data->argc, data->argv);
97 enum exception_class {
98 NO_EXCEPTION = 0,
99 EXCEPTION_NO_METHOD_ERROR,
100 EXCEPTION_OTHER,
103 static VALUE
104 funcall2 (VALUE receiver, ID method_id, int argc, volatile VALUE *argv,
105 enum exception_class *exception_happened)
107 struct callback_data data;
108 size_t i, len;
109 int state = 0;
110 volatile VALUE ret, exn, message, backtrace, b;
112 assert (argc <= MAX_ARGS);
114 data.receiver = receiver;
115 data.method_id = method_id;
116 data.argc = argc;
117 for (i = 0; i < argc; ++i)
118 data.argv[i] = argv[i];
120 ret = rb_protect (callback_dispatch, (VALUE) &data, &state);
121 if (state) {
122 /* An exception was thrown. Get the per-thread exception. */
123 exn = rb_errinfo ();
125 /* We treat NoMethodError specially. */
126 if (rb_obj_is_kind_of (exn, rb_eNoMethodError)) {
127 if (exception_happened)
128 *exception_happened = EXCEPTION_NO_METHOD_ERROR;
130 else {
131 if (exception_happened)
132 *exception_happened = EXCEPTION_OTHER;
134 /* Print the exception. */
135 message = rb_funcall (exn, rb_intern ("to_s"), 0);
136 nbdkit_error ("ruby: %s", StringValueCStr (message));
138 /* Try to print the backtrace (a list of strings) if it exists. */
139 backtrace = rb_funcall (exn, rb_intern ("backtrace"), 0);
140 if (! NIL_P (backtrace)) {
141 len = RARRAY_LEN (backtrace);
142 for (i = 0; i < len; ++i) {
143 b = rb_ary_entry (backtrace, i);
144 nbdkit_error ("ruby: frame #%zu %s", i, StringValueCStr (b));
149 /* Reset the current thread exception. */
150 rb_set_errinfo (Qnil);
151 return Qnil;
153 else {
154 if (exception_happened)
155 *exception_happened = NO_EXCEPTION;
156 return ret;
160 static const char *script = NULL;
161 static void *code = NULL;
163 static void
164 plugin_rb_unload (void)
166 if (ruby_cleanup (0) != 0)
167 nbdkit_error ("ruby_cleanup failed");
170 static void
171 plugin_rb_dump_plugin (void)
173 #ifdef RUBY_API_VERSION_MAJOR
174 printf ("ruby_api_version=%d", RUBY_API_VERSION_MAJOR);
175 #ifdef RUBY_API_VERSION_MINOR
176 printf (".%d", RUBY_API_VERSION_MINOR);
177 #ifdef RUBY_API_VERSION_TEENY
178 printf (".%d", RUBY_API_VERSION_TEENY);
179 #endif
180 #endif
181 printf ("\n");
182 #endif
184 if (!script)
185 return;
187 assert (code != NULL);
189 (void) funcall2 (Qnil, rb_intern ("dump_plugin"), 0, NULL, NULL);
192 static int
193 plugin_rb_config (const char *key, const char *value)
195 /* The first parameter must be "script". */
196 if (!script) {
197 int state;
199 if (strcmp (key, "script") != 0) {
200 nbdkit_error ("the first parameter must be script=/path/to/ruby/script.rb");
201 return -1;
203 script = value;
205 nbdkit_debug ("ruby: loading script %s", script);
207 /* Load the Ruby script into the interpreter. */
208 const char *options[] = { "--", script };
209 code = ruby_options (sizeof options / sizeof options[0],
210 (char **) options);
212 /* Check if we managed to compile the Ruby script to code. */
213 if (!ruby_executable_node (code, &state)) {
214 nbdkit_error ("could not compile ruby script (%s, state=%d)",
215 script, state);
216 return -1;
219 /* Execute the Ruby script. */
220 state = ruby_exec_node (code);
221 if (state) {
222 nbdkit_error ("could not execute ruby script (%s, state=%d)",
223 script, state);
224 return -1;
227 return 0;
229 else {
230 volatile VALUE argv[2];
231 enum exception_class exception_happened;
233 argv[0] = rb_str_new2 (key);
234 argv[1] = rb_str_new2 (value);
235 (void) funcall2 (Qnil, rb_intern ("config"), 2, argv, &exception_happened);
236 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
237 /* No config method, emulate what core nbdkit does if the
238 * config callback is NULL.
240 nbdkit_error ("%s: this plugin does not need command line configuration",
241 script);
242 return -1;
244 else if (exception_happened == EXCEPTION_OTHER)
245 return -1;
247 return 0;
251 static int
252 plugin_rb_config_complete (void)
254 enum exception_class exception_happened;
256 if (!script) {
257 nbdkit_error ("the first parameter must be script=/path/to/ruby/script.rb");
258 return -1;
261 assert (code != NULL);
263 (void) funcall2 (Qnil, rb_intern ("config_complete"), 0, NULL,
264 &exception_happened);
265 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
266 return 0; /* no config_complete method defined, ignore */
267 else if (exception_happened == EXCEPTION_OTHER)
268 return -1;
270 return 0;
273 static void *
274 plugin_rb_open (int readonly)
276 volatile VALUE argv[1];
277 volatile VALUE rv;
278 enum exception_class exception_happened;
280 argv[0] = readonly ? Qtrue : Qfalse;
281 rv = funcall2 (Qnil, rb_intern ("open"), 1, argv, &exception_happened);
282 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
283 nbdkit_error ("%s: missing callback: %s", script, "open");
284 return NULL;
286 else if (exception_happened == EXCEPTION_OTHER)
287 return NULL;
289 return (void *) rv;
292 static void
293 plugin_rb_close (void *handle)
295 volatile VALUE argv[1];
297 argv[0] = (VALUE) handle;
298 (void) funcall2 (Qnil, rb_intern ("close"), 1, argv, NULL);
299 /* OK to ignore exceptions here, if they are important then an error
300 * was printed already.
304 static int64_t
305 plugin_rb_get_size (void *handle)
307 volatile VALUE argv[1];
308 volatile VALUE rv;
309 enum exception_class exception_happened;
311 argv[0] = (VALUE) handle;
312 rv = funcall2 (Qnil, rb_intern ("get_size"), 1, argv, &exception_happened);
313 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
314 nbdkit_error ("%s: missing callback: %s", script, "get_size");
315 return -1;
317 else if (exception_happened == EXCEPTION_OTHER)
318 return -1;
320 return NUM2ULL (rv);
323 static int
324 plugin_rb_pread (void *handle, void *buf,
325 uint32_t count, uint64_t offset)
327 volatile VALUE argv[3];
328 volatile VALUE rv;
329 enum exception_class exception_happened;
331 argv[0] = (VALUE) handle;
332 argv[1] = ULL2NUM (count);
333 argv[2] = ULL2NUM (offset);
334 rv = funcall2 (Qnil, rb_intern ("pread"), 3, argv, &exception_happened);
335 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
336 nbdkit_error ("%s: missing callback: %s", script, "pread");
337 return -1;
339 else if (exception_happened == EXCEPTION_OTHER)
340 return -1;
342 if (RSTRING_LEN (rv) < count) {
343 nbdkit_error ("%s: byte array returned from pread is too small",
344 script);
345 return -1;
348 memcpy (buf, RSTRING_PTR (rv), count);
349 return 0;
352 static int
353 plugin_rb_pwrite (void *handle, const void *buf,
354 uint32_t count, uint64_t offset)
356 volatile VALUE argv[3];
357 enum exception_class exception_happened;
359 argv[0] = (VALUE) handle;
360 argv[1] = rb_str_new (buf, count);
361 argv[2] = ULL2NUM (offset);
362 (void) funcall2 (Qnil, rb_intern ("pwrite"), 3, argv, &exception_happened);
363 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
364 nbdkit_error ("%s: missing callback: %s", script, "pwrite");
365 return -1;
367 else if (exception_happened == EXCEPTION_OTHER)
368 return -1;
370 return 0;
373 static int
374 plugin_rb_flush (void *handle)
376 volatile VALUE argv[1];
377 enum exception_class exception_happened;
379 argv[0] = (VALUE) handle;
380 (void) funcall2 (Qnil, rb_intern ("flush"), 1, argv, &exception_happened);
381 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
382 nbdkit_error ("%s: not implemented: %s", script, "flush");
383 return -1;
385 else if (exception_happened == EXCEPTION_OTHER)
386 return -1;
388 return 0;
391 static int
392 plugin_rb_trim (void *handle, uint32_t count, uint64_t offset)
394 volatile VALUE argv[3];
395 enum exception_class exception_happened;
397 argv[0] = (VALUE) handle;
398 argv[1] = ULL2NUM (count);
399 argv[2] = ULL2NUM (offset);
400 (void) funcall2 (Qnil, rb_intern ("trim"), 3, argv, &exception_happened);
401 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
402 nbdkit_error ("%s: not implemented: %s", script, "trim");
403 return -1;
405 else if (exception_happened == EXCEPTION_OTHER)
406 return -1;
408 return 0;
411 static int
412 plugin_rb_zero (void *handle, uint32_t count, uint64_t offset, int may_trim)
414 volatile VALUE argv[4];
415 enum exception_class exception_happened;
417 argv[0] = (VALUE) handle;
418 argv[1] = ULL2NUM (count);
419 argv[2] = ULL2NUM (offset);
420 argv[3] = may_trim ? Qtrue : Qfalse;
421 last_error = 0;
422 (void) funcall2 (Qnil, rb_intern ("zero"), 4, argv, &exception_happened);
423 if (last_error == EOPNOTSUPP || last_error == ENOTSUP ||
424 exception_happened == EXCEPTION_NO_METHOD_ERROR) {
425 nbdkit_debug ("zero falling back to pwrite");
426 nbdkit_set_error (EOPNOTSUPP);
427 return -1;
429 else if (exception_happened == EXCEPTION_OTHER)
430 return -1;
432 return 0;
435 static int
436 plugin_rb_can_write (void *handle)
438 volatile VALUE argv[1];
439 volatile VALUE rv;
440 enum exception_class exception_happened;
442 argv[0] = (VALUE) handle;
443 rv = funcall2 (Qnil, rb_intern ("can_write"), 1, argv, &exception_happened);
444 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
445 /* Fall back to checking if the pwrite method exists. */
446 rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
447 2, ID2SYM (rb_intern ("pwrite")), Qtrue);
448 else if (exception_happened == EXCEPTION_OTHER)
449 return -1;
451 return RTEST (rv);
454 static int
455 plugin_rb_can_flush (void *handle)
457 volatile VALUE argv[1];
458 volatile VALUE rv;
459 enum exception_class exception_happened;
461 argv[0] = (VALUE) handle;
462 rv = funcall2 (Qnil, rb_intern ("can_flush"), 1, argv, &exception_happened);
463 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
464 /* Fall back to checking if the flush method exists. */
465 rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
466 2, ID2SYM (rb_intern ("flush")), Qtrue);
467 else if (exception_happened == EXCEPTION_OTHER)
468 return -1;
470 return RTEST (rv);
473 static int
474 plugin_rb_is_rotational (void *handle)
476 volatile VALUE argv[1];
477 volatile VALUE rv;
478 enum exception_class exception_happened;
480 argv[0] = (VALUE) handle;
481 rv = funcall2 (Qnil, rb_intern ("is_rotational"), 1, argv,
482 &exception_happened);
483 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
484 return 0;
485 else if (exception_happened == EXCEPTION_OTHER)
486 return -1;
488 return RTEST (rv);
491 static int
492 plugin_rb_can_trim (void *handle)
494 volatile VALUE argv[1];
495 volatile VALUE rv;
496 enum exception_class exception_happened;
498 argv[0] = (VALUE) handle;
499 rv = funcall2 (Qnil, rb_intern ("can_trim"), 1, argv, &exception_happened);
500 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
501 /* Fall back to checking if the trim method exists. */
502 rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
503 2, ID2SYM (rb_intern ("trim")), Qtrue);
504 else if (exception_happened == EXCEPTION_OTHER)
505 return -1;
507 return RTEST (rv);
510 #define plugin_rb_config_help \
511 "script=<FILENAME> (required) The Ruby plugin to run.\n" \
512 "[other arguments may be used by the plugin that you load]"
514 /* Ruby is inherently unsafe to call in parallel from multiple
515 * threads.
517 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_ALL_REQUESTS
519 static struct nbdkit_plugin plugin = {
520 .name = "ruby",
521 .version = PACKAGE_VERSION,
523 .load = plugin_rb_load,
524 .unload = plugin_rb_unload,
525 .dump_plugin = plugin_rb_dump_plugin,
527 .config = plugin_rb_config,
528 .config_complete = plugin_rb_config_complete,
529 .config_help = plugin_rb_config_help,
531 .open = plugin_rb_open,
532 .close = plugin_rb_close,
534 .get_size = plugin_rb_get_size,
535 .can_write = plugin_rb_can_write,
536 .can_flush = plugin_rb_can_flush,
537 .is_rotational = plugin_rb_is_rotational,
538 .can_trim = plugin_rb_can_trim,
540 .pread = plugin_rb_pread,
541 .pwrite = plugin_rb_pwrite,
542 .flush = plugin_rb_flush,
543 .trim = plugin_rb_trim,
544 .zero = plugin_rb_zero,
547 NBDKIT_REGISTER_PLUGIN(plugin)