714 lines
26 KiB
C
714 lines
26 KiB
C
// mrubyAdapter.c -- mruby (Ruby) engine adapter for the broker.
|
|
//
|
|
// Each exposed native is a Kernel method bound to one shared trampoline. mrb_func_t carries no
|
|
// userData, so the trampoline recovers the native's name from the current call's method id
|
|
// (mrb_get_mid) and the owning context from mrb->ud, then marshals the Ruby arguments to
|
|
// CalogValueT and dispatches through calogCall (honoring the actor route hook). Marshalling covers
|
|
// scalars and binary-safe strings; aggregates cross both ways (Array<->list, Hash<->map). Functions
|
|
// cross both ways too: a Ruby proc out becomes a refcounted CalogFnT pinned as a GC root
|
|
// (mrb_gc_register) and dropped on release; a foreign CalogFnT in becomes a real Ruby Proc
|
|
// (mrb_proc_new_cfunc_with_env) carrying the CalogFnT in its env -- the script calls it as f.call(x).
|
|
// Foreign functions are tracked on the context and released at destroy (a cfunc proc's env has no
|
|
// per-value finalizer). puts/print/p are provided (routed to stdout); there is no file/socket IO.
|
|
//
|
|
// Error model: on failure the trampoline raises an mruby exception (mrb_raise, which longjmps out
|
|
// of the VM) after freeing any CalogValueT it owns. GC-arena save/restore brackets marshalling so
|
|
// transient objects created here do not overflow the arena.
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "mrubyAdapter.h"
|
|
|
|
#include <mruby.h>
|
|
#include <mruby/array.h>
|
|
#include <mruby/compile.h>
|
|
#include <mruby/error.h>
|
|
#include <mruby/hash.h>
|
|
#include <mruby/proc.h>
|
|
#include <mruby/string.h>
|
|
#include <mruby/variable.h>
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define MRUBY_FOREIGN_INITIAL 8
|
|
|
|
struct CalogMrubyT {
|
|
mrb_state *mrb;
|
|
CalogT *broker;
|
|
uint64_t ctxId;
|
|
CalogFnT **foreignFns; // foreign function values pushed into this VM
|
|
int32_t foreignCount;
|
|
int32_t foreignCap;
|
|
};
|
|
|
|
// Backs a CalogFnT exported from this VM: the owning context and the pinned Ruby callable
|
|
// (a Proc/Method), kept GC-reachable by mrb_gc_register until the CalogFnT is released.
|
|
typedef struct MrubyExportT {
|
|
CalogMrubyT *context;
|
|
mrb_value callable;
|
|
} MrubyExportT;
|
|
|
|
static int32_t mrubyCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static void mrubyCallableRelease(CalogFnT *callable);
|
|
static int32_t mrubyExportValue(CalogMrubyT *context, mrb_value callable, CalogFnT **out);
|
|
static mrb_value mrubyForeignCall(mrb_state *mrb, mrb_value self);
|
|
static int32_t mrubyFromValue(CalogMrubyT *context, const CalogValueT *value, mrb_value *out, int32_t depth);
|
|
static int32_t mrubyInvokeArgs(CalogMrubyT *context, CalogFnT *callable, mrb_value *argv, mrb_int argc, mrb_value *out, char *errbuf, size_t errcap);
|
|
static mrb_value mrubyMethodMissing(mrb_state *mrb, mrb_value self);
|
|
static mrb_value mrubyP(mrb_state *mrb, mrb_value self);
|
|
static mrb_value mrubyPrint(mrb_state *mrb, mrb_value self);
|
|
static mrb_value mrubyPuts(mrb_state *mrb, mrb_value self);
|
|
static int32_t mrubyToValue(CalogMrubyT *context, mrb_value value, CalogValueT *out, int32_t depth);
|
|
static int32_t mrubyTrackForeign(CalogMrubyT *context, CalogFnT *callable);
|
|
static mrb_value mrubyTrampoline(mrb_state *mrb, mrb_value self);
|
|
|
|
|
|
int32_t calogMrubyCreate(CalogMrubyT **out, CalogT *broker, uint64_t ctxId) {
|
|
CalogMrubyT *context;
|
|
mrb_state *mrb;
|
|
|
|
*out = NULL;
|
|
context = (CalogMrubyT *)calloc(1, sizeof(*context));
|
|
if (context == NULL) {
|
|
return calogErrOomE;
|
|
}
|
|
mrb = mrb_open();
|
|
if (mrb == NULL) {
|
|
free(context);
|
|
return calogErrOomE;
|
|
}
|
|
context->mrb = mrb;
|
|
context->broker = broker;
|
|
context->ctxId = ctxId;
|
|
mrb->ud = context; // the trampoline recovers the context from here
|
|
// Ruby ergonomics without pulling mruby-io: puts/print/p route to stdout. Defined on Kernel
|
|
// so a bare `puts "x"` works at top level (calog scripts still use fs*/net* for real IO).
|
|
mrb_define_method(mrb, mrb->kernel_module, "puts", mrubyPuts, MRB_ARGS_ANY());
|
|
mrb_define_method(mrb, mrb->kernel_module, "print", mrubyPrint, MRB_ARGS_ANY());
|
|
mrb_define_method(mrb, mrb->kernel_module, "p", mrubyP, MRB_ARGS_ANY());
|
|
// Bare-name export resolution: a top-level `exportedFn(args)` that is not a defined method
|
|
// resolves to a broker export (see mrubyMethodMissing), like the hook engines.
|
|
mrb_define_method(mrb, mrb->kernel_module, "method_missing", mrubyMethodMissing, MRB_ARGS_ANY());
|
|
*out = context;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t mrubyCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
MrubyExportT *export;
|
|
CalogMrubyT *context;
|
|
mrb_state *mrb;
|
|
mrb_value *argv;
|
|
mrb_value ret;
|
|
int arena;
|
|
int32_t index;
|
|
int32_t status;
|
|
|
|
export = (MrubyExportT *)userData;
|
|
context = export->context;
|
|
mrb = context->mrb;
|
|
calogValueNil(result);
|
|
argv = NULL;
|
|
if (argCount > 0) {
|
|
argv = (mrb_value *)calloc((size_t)argCount, sizeof(mrb_value));
|
|
if (argv == NULL) {
|
|
return calogFail(result, calogErrOomE, "out of memory calling mruby callable");
|
|
}
|
|
}
|
|
arena = mrb_gc_arena_save(mrb);
|
|
for (index = 0; index < argCount; index++) {
|
|
status = mrubyFromValue(context, &args[index], &argv[index], 0);
|
|
if (status != calogOkE) {
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
free(argv);
|
|
return calogFail(result, status, "failed to marshal argument into mruby");
|
|
}
|
|
}
|
|
ret = mrb_funcall_argv(mrb, export->callable, mrb_intern_lit(mrb, "call"), (mrb_int)argCount, argv);
|
|
free(argv);
|
|
if (mrb->exc != NULL) {
|
|
mrb_value exc;
|
|
mrb_value message;
|
|
const char *text;
|
|
exc = mrb_obj_value(mrb->exc);
|
|
mrb->exc = NULL;
|
|
message = mrb_funcall_argv(mrb, exc, mrb_intern_lit(mrb, "message"), 0, NULL);
|
|
text = mrb_string_p(message) ? RSTRING_PTR(message) : "mruby callable raised";
|
|
status = calogFail(result, calogErrArgE, text);
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
return status;
|
|
}
|
|
status = mrubyToValue(context, ret, result, 0);
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
return status;
|
|
}
|
|
|
|
|
|
static void mrubyCallableRelease(CalogFnT *callable) {
|
|
MrubyExportT *export;
|
|
|
|
export = (MrubyExportT *)calogFnUserData(callable);
|
|
mrb_gc_unregister(export->context->mrb, export->callable);
|
|
free(export);
|
|
}
|
|
|
|
|
|
void calogMrubyDestroy(CalogMrubyT *context) {
|
|
int32_t index;
|
|
|
|
if (context == NULL) {
|
|
return;
|
|
}
|
|
// Release the foreign function values pushed into this VM (see mrubyTrackForeign): a cfunc
|
|
// proc's env has no per-value finalizer, so they are held until the context is destroyed.
|
|
for (index = 0; index < context->foreignCount; index++) {
|
|
calogFnRelease(context->foreignFns[index]);
|
|
}
|
|
free(context->foreignFns);
|
|
if (context->mrb != NULL) {
|
|
mrb_close(context->mrb);
|
|
}
|
|
free(context);
|
|
}
|
|
|
|
|
|
int32_t calogMrubyExport(CalogMrubyT *context, const char *name, CalogFnT **out) {
|
|
mrb_state *mrb;
|
|
mrb_value self;
|
|
mrb_value method;
|
|
mrb_value sym;
|
|
int arena;
|
|
int32_t status;
|
|
|
|
mrb = context->mrb;
|
|
*out = NULL;
|
|
// Object#method returns a Method for a top-level `def name` (public or private). A Proc/lambda
|
|
// held in a global ($name) also resolves through method_missing/global lookup below.
|
|
self = mrb_top_self(mrb);
|
|
sym = mrb_symbol_value(mrb_intern_cstr(mrb, name));
|
|
arena = mrb_gc_arena_save(mrb);
|
|
method = mrb_funcall_argv(mrb, self, mrb_intern_lit(mrb, "method"), 1, &sym);
|
|
if (mrb->exc != NULL) {
|
|
mrb->exc = NULL;
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
return calogErrNotFoundE;
|
|
}
|
|
status = mrubyExportValue(context, method, out);
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
return status;
|
|
}
|
|
|
|
|
|
static int32_t mrubyExportValue(CalogMrubyT *context, mrb_value callable, CalogFnT **out) {
|
|
mrb_state *mrb;
|
|
MrubyExportT *export;
|
|
int32_t status;
|
|
|
|
mrb = context->mrb;
|
|
*out = NULL;
|
|
export = (MrubyExportT *)malloc(sizeof(*export));
|
|
if (export == NULL) {
|
|
return calogErrOomE;
|
|
}
|
|
export->context = context;
|
|
export->callable = callable;
|
|
// Pin the callable as a GC root so it survives until the CalogFnT is released.
|
|
mrb_gc_register(mrb, callable);
|
|
status = calogFnCreate(out, context->broker, mrubyCallableInvoke, export, mrubyCallableRelease, context->ctxId);
|
|
if (status != calogOkE) {
|
|
mrb_gc_unregister(mrb, callable);
|
|
free(export);
|
|
return status;
|
|
}
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
int32_t calogMrubyExpose(CalogMrubyT *context, const char *name) {
|
|
mrb_state *mrb;
|
|
|
|
mrb = context->mrb;
|
|
if (calogLookup(context->broker, name) == NULL) {
|
|
return calogErrNotFoundE;
|
|
}
|
|
// A public Kernel method routed to the shared trampoline, so `name(args)` works at top level
|
|
// in any context. The trampoline recovers `name` from the call's method id.
|
|
mrb_define_method(mrb, mrb->kernel_module, name, mrubyTrampoline, MRB_ARGS_ANY());
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
// Call target for a foreign CalogFnT pushed into mruby as a Proc. The CalogFnT travels in the
|
|
// proc's cfunc env (slot 0, a cptr); the context comes from mrb->ud.
|
|
static mrb_value mrubyForeignCall(mrb_state *mrb, mrb_value self) {
|
|
CalogMrubyT *context;
|
|
CalogFnT *callable;
|
|
mrb_value *argv;
|
|
mrb_int argc;
|
|
mrb_value out;
|
|
int arena;
|
|
int32_t status;
|
|
char message[CALOG_ERR_MSG_CAP];
|
|
|
|
(void)self;
|
|
context = (CalogMrubyT *)mrb->ud;
|
|
callable = (CalogFnT *)mrb_cptr(mrb_proc_cfunc_env_get(mrb, 0));
|
|
mrb_get_args(mrb, "*", &argv, &argc);
|
|
arena = mrb_gc_arena_save(mrb);
|
|
status = mrubyInvokeArgs(context, callable, argv, argc, &out, message, sizeof(message));
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
if (status != calogOkE) {
|
|
mrb_raise(mrb, E_RUNTIME_ERROR, message);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
static int32_t mrubyFromValue(CalogMrubyT *context, const CalogValueT *value, mrb_value *out, int32_t depth) {
|
|
mrb_state *mrb;
|
|
|
|
mrb = context->mrb;
|
|
*out = mrb_nil_value();
|
|
if (depth >= CALOG_MAX_DEPTH) {
|
|
return calogErrDepthE;
|
|
}
|
|
switch (value->type) {
|
|
case calogNilE:
|
|
return calogOkE;
|
|
case calogBoolE:
|
|
*out = mrb_bool_value(value->as.b);
|
|
return calogOkE;
|
|
case calogIntE:
|
|
*out = mrb_int_value(mrb, (mrb_int)value->as.i);
|
|
return calogOkE;
|
|
case calogRealE:
|
|
*out = mrb_float_value(mrb, (mrb_float)value->as.r);
|
|
return calogOkE;
|
|
case calogStringE:
|
|
*out = mrb_str_new(mrb, value->as.s.bytes, (mrb_int)value->as.s.length);
|
|
return calogOkE;
|
|
case calogAggE: {
|
|
CalogAggT *aggregate;
|
|
int64_t index;
|
|
int32_t status;
|
|
mrb_value child;
|
|
aggregate = value->as.agg;
|
|
if (calogAggIsKeyed(aggregate)) {
|
|
mrb_value map;
|
|
map = mrb_hash_new(mrb);
|
|
for (index = 0; index < aggregate->arrayCount; index++) {
|
|
status = mrubyFromValue(context, &aggregate->array[index], &child, depth + 1);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
mrb_hash_set(mrb, map, mrb_int_value(mrb, (mrb_int)index), child);
|
|
}
|
|
for (index = 0; index < aggregate->pairCount; index++) {
|
|
mrb_value key;
|
|
status = mrubyFromValue(context, &aggregate->pairs[index].key, &key, depth + 1);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
status = mrubyFromValue(context, &aggregate->pairs[index].value, &child, depth + 1);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
mrb_hash_set(mrb, map, key, child);
|
|
}
|
|
*out = map;
|
|
return calogOkE;
|
|
}
|
|
*out = mrb_ary_new_capa(mrb, (mrb_int)aggregate->arrayCount);
|
|
for (index = 0; index < aggregate->arrayCount; index++) {
|
|
status = mrubyFromValue(context, &aggregate->array[index], &child, depth + 1);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
mrb_ary_push(mrb, *out, child);
|
|
}
|
|
return calogOkE;
|
|
}
|
|
case calogFnE: {
|
|
// A foreign callable becomes a real Ruby Proc carrying the CalogFnT in its env;
|
|
// the script calls it as f.call(x) / f.(x). Tracked on the context and released at
|
|
// destroy (a cfunc proc's env has no per-value finalizer).
|
|
mrb_value env[1];
|
|
if (mrubyTrackForeign(context, value->as.fn) != calogOkE) {
|
|
return calogErrOomE;
|
|
}
|
|
env[0] = mrb_cptr_value(mrb, value->as.fn);
|
|
*out = mrb_obj_value(mrb_proc_new_cfunc_with_env(mrb, mrubyForeignCall, 1, env));
|
|
calogFnRetain(value->as.fn);
|
|
return calogOkE;
|
|
}
|
|
}
|
|
return calogErrTypeE;
|
|
}
|
|
|
|
|
|
// Marshal argv[0..argc) into CalogValueT, invoke `callable`, and marshal the result into *out.
|
|
// Non-raising: returns calogOkE (out set) or an error, copying a message into errbuf so the caller
|
|
// can release its own references before raising (the mruby raise longjmps out).
|
|
static int32_t mrubyInvokeArgs(CalogMrubyT *context, CalogFnT *callable, mrb_value *argv, mrb_int argc, mrb_value *out, char *errbuf, size_t errcap) {
|
|
CalogValueT *args;
|
|
CalogValueT result;
|
|
mrb_int index;
|
|
int32_t status;
|
|
|
|
*out = mrb_nil_value();
|
|
args = NULL;
|
|
if (argc > 0) {
|
|
args = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT));
|
|
if (args == NULL) {
|
|
snprintf(errbuf, errcap, "out of memory marshalling arguments");
|
|
return calogErrOomE;
|
|
}
|
|
}
|
|
for (index = 0; index < argc; index++) {
|
|
status = mrubyToValue(context, argv[index], &args[index], 0);
|
|
if (status != calogOkE) {
|
|
mrb_int cleanup;
|
|
for (cleanup = 0; cleanup < index; cleanup++) {
|
|
calogValueFree(&args[cleanup]);
|
|
}
|
|
free(args);
|
|
snprintf(errbuf, errcap, "failed to marshal an argument");
|
|
return status;
|
|
}
|
|
}
|
|
status = calogFnInvoke(callable, args, (int32_t)argc, &result);
|
|
for (index = 0; index < argc; index++) {
|
|
calogValueFree(&args[index]);
|
|
}
|
|
free(args);
|
|
if (status != calogOkE) {
|
|
snprintf(errbuf, errcap, "%s", (result.type == calogStringE) ? result.as.s.bytes : "call failed");
|
|
calogValueFree(&result);
|
|
return status;
|
|
}
|
|
status = mrubyFromValue(context, &result, out, 0);
|
|
calogValueFree(&result);
|
|
if (status != calogOkE) {
|
|
snprintf(errbuf, errcap, "failed to marshal the result");
|
|
}
|
|
return status;
|
|
}
|
|
|
|
|
|
// Kernel#method_missing: a bare name called at TOP LEVEL that is not a defined method may be a
|
|
// broker export. Resolve and invoke it -- but only when self is the top-level main object, so a
|
|
// missing method on any other receiver (obj.foo) stays a NoMethodError and is never hijacked.
|
|
// This gives Ruby the bare-name export resolution the hook engines have (no VM change needed).
|
|
static mrb_value mrubyMethodMissing(mrb_state *mrb, mrb_value self) {
|
|
CalogMrubyT *context;
|
|
mrb_value *argv;
|
|
mrb_int argc;
|
|
mrb_sym missing;
|
|
const char *name;
|
|
CalogValueT nameArg;
|
|
CalogValueT resolved;
|
|
mrb_value out;
|
|
int arena;
|
|
int32_t status;
|
|
char message[CALOG_ERR_MSG_CAP];
|
|
|
|
context = (CalogMrubyT *)mrb->ud;
|
|
mrb_get_args(mrb, "*", &argv, &argc); // argv[0] is the method name (a Symbol), rest are args
|
|
if (argc < 1 || !mrb_symbol_p(argv[0])) {
|
|
mrb_raise(mrb, E_NOMETHOD_ERROR, "method_missing without a method name");
|
|
}
|
|
missing = mrb_symbol(argv[0]);
|
|
if (!mrb_obj_equal(mrb, self, mrb_top_self(mrb))) {
|
|
mrb_raisef(mrb, E_NOMETHOD_ERROR, "undefined method '%n'", missing);
|
|
}
|
|
name = mrb_sym_name(mrb, missing);
|
|
if (calogValueString(&nameArg, name, (int64_t)strlen(name)) != calogOkE) {
|
|
mrb_raisef(mrb, E_NOMETHOD_ERROR, "undefined method '%n'", missing);
|
|
}
|
|
calogValueNil(&resolved);
|
|
calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &resolved);
|
|
calogValueFree(&nameArg);
|
|
if (resolved.type != calogFnE) {
|
|
calogValueFree(&resolved);
|
|
mrb_raisef(mrb, E_NOMETHOD_ERROR, "undefined method '%n'", missing);
|
|
}
|
|
// It is an export: invoke with the remaining args. Keep our resolved reference across the
|
|
// invoke (guards against a concurrent unexport), release it, THEN raise on failure -- so the
|
|
// reference never leaks on the raise path.
|
|
arena = mrb_gc_arena_save(mrb);
|
|
status = mrubyInvokeArgs(context, resolved.as.fn, argv + 1, argc - 1, &out, message, sizeof(message));
|
|
calogValueFree(&resolved);
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
if (status != calogOkE) {
|
|
mrb_raise(mrb, E_RUNTIME_ERROR, message);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
static mrb_value mrubyP(mrb_state *mrb, mrb_value self) {
|
|
mrb_value *argv;
|
|
mrb_int argc;
|
|
mrb_int index;
|
|
|
|
(void)self;
|
|
mrb_get_args(mrb, "*", &argv, &argc);
|
|
for (index = 0; index < argc; index++) {
|
|
mrb_value text;
|
|
text = mrb_inspect(mrb, argv[index]);
|
|
fwrite(RSTRING_PTR(text), 1, (size_t)RSTRING_LEN(text), stdout);
|
|
fputc('\n', stdout);
|
|
}
|
|
if (argc == 1) {
|
|
return argv[0];
|
|
}
|
|
return mrb_nil_value();
|
|
}
|
|
|
|
|
|
static mrb_value mrubyPrint(mrb_state *mrb, mrb_value self) {
|
|
mrb_value *argv;
|
|
mrb_int argc;
|
|
mrb_int index;
|
|
|
|
(void)self;
|
|
mrb_get_args(mrb, "*", &argv, &argc);
|
|
for (index = 0; index < argc; index++) {
|
|
mrb_value text;
|
|
text = mrb_obj_as_string(mrb, argv[index]);
|
|
fwrite(RSTRING_PTR(text), 1, (size_t)RSTRING_LEN(text), stdout);
|
|
}
|
|
return mrb_nil_value();
|
|
}
|
|
|
|
|
|
static mrb_value mrubyPuts(mrb_state *mrb, mrb_value self) {
|
|
mrb_value *argv;
|
|
mrb_int argc;
|
|
mrb_int index;
|
|
|
|
(void)self;
|
|
mrb_get_args(mrb, "*", &argv, &argc);
|
|
if (argc == 0) {
|
|
fputc('\n', stdout);
|
|
return mrb_nil_value();
|
|
}
|
|
for (index = 0; index < argc; index++) {
|
|
mrb_value text;
|
|
text = mrb_obj_as_string(mrb, argv[index]);
|
|
fwrite(RSTRING_PTR(text), 1, (size_t)RSTRING_LEN(text), stdout);
|
|
fputc('\n', stdout);
|
|
}
|
|
return mrb_nil_value();
|
|
}
|
|
|
|
|
|
int32_t calogMrubyRun(CalogMrubyT *context, const char *source) {
|
|
mrb_state *mrb;
|
|
int arena;
|
|
|
|
mrb = context->mrb;
|
|
arena = mrb_gc_arena_save(mrb);
|
|
mrb_load_string(mrb, source);
|
|
if (mrb->exc != NULL) {
|
|
mrb_value exc;
|
|
mrb_value text;
|
|
exc = mrb_obj_value(mrb->exc);
|
|
mrb->exc = NULL;
|
|
text = mrb_funcall_argv(mrb, exc, mrb_intern_lit(mrb, "inspect"), 0, NULL);
|
|
fprintf(stderr, "mruby error: %s\n", mrb_string_p(text) ? RSTRING_PTR(text) : "(unknown)");
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
return calogErrArgE;
|
|
}
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t mrubyToValue(CalogMrubyT *context, mrb_value value, CalogValueT *out, int32_t depth) {
|
|
mrb_state *mrb;
|
|
|
|
mrb = context->mrb;
|
|
calogValueNil(out);
|
|
if (depth >= CALOG_MAX_DEPTH) {
|
|
return calogErrDepthE;
|
|
}
|
|
if (mrb_nil_p(value)) {
|
|
return calogOkE;
|
|
}
|
|
switch (mrb_type(value)) {
|
|
case MRB_TT_TRUE:
|
|
calogValueBool(out, true);
|
|
return calogOkE;
|
|
case MRB_TT_FALSE:
|
|
calogValueBool(out, false);
|
|
return calogOkE;
|
|
case MRB_TT_INTEGER:
|
|
calogValueInt(out, (int64_t)mrb_integer(value));
|
|
return calogOkE;
|
|
case MRB_TT_FLOAT:
|
|
calogValueReal(out, (double)mrb_float(value));
|
|
return calogOkE;
|
|
case MRB_TT_STRING:
|
|
return calogValueString(out, RSTRING_PTR(value), (int64_t)RSTRING_LEN(value));
|
|
case MRB_TT_ARRAY: {
|
|
CalogAggT *aggregate;
|
|
int32_t status;
|
|
mrb_int index;
|
|
mrb_int count;
|
|
status = calogAggCreate(&aggregate, calogListE);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
count = RARRAY_LEN(value);
|
|
for (index = 0; index < count; index++) {
|
|
CalogValueT element;
|
|
status = mrubyToValue(context, mrb_ary_ref(mrb, value, index), &element, depth + 1);
|
|
if (status != calogOkE) {
|
|
calogAggFree(aggregate);
|
|
return status;
|
|
}
|
|
status = calogAggPush(aggregate, &element);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&element);
|
|
calogAggFree(aggregate);
|
|
return status;
|
|
}
|
|
}
|
|
calogValueAgg(out, aggregate);
|
|
return calogOkE;
|
|
}
|
|
case MRB_TT_HASH: {
|
|
CalogAggT *aggregate;
|
|
mrb_value keys;
|
|
int32_t status;
|
|
mrb_int index;
|
|
mrb_int count;
|
|
status = calogAggCreate(&aggregate, calogMapE);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
keys = mrb_hash_keys(mrb, value);
|
|
count = RARRAY_LEN(keys);
|
|
for (index = 0; index < count; index++) {
|
|
mrb_value rubyKey;
|
|
CalogValueT key;
|
|
CalogValueT element;
|
|
rubyKey = mrb_ary_ref(mrb, keys, index);
|
|
status = mrubyToValue(context, rubyKey, &key, depth + 1);
|
|
if (status != calogOkE) {
|
|
calogAggFree(aggregate);
|
|
return status;
|
|
}
|
|
status = mrubyToValue(context, mrb_hash_get(mrb, value, rubyKey), &element, depth + 1);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&key);
|
|
calogAggFree(aggregate);
|
|
return status;
|
|
}
|
|
status = calogAggSet(aggregate, &key, &element);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&key);
|
|
calogValueFree(&element);
|
|
calogAggFree(aggregate);
|
|
return status;
|
|
}
|
|
}
|
|
calogValueAgg(out, aggregate);
|
|
return calogOkE;
|
|
}
|
|
case MRB_TT_PROC: {
|
|
CalogFnT *callable;
|
|
int32_t status;
|
|
status = mrubyExportValue(context, value, &callable);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
calogValueFn(out, callable);
|
|
return calogOkE;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
// Other reference types (symbols, objects, ranges, ...) have no CalogValueT equivalent.
|
|
return calogErrUnsupportedE;
|
|
}
|
|
|
|
|
|
// Record a foreign function pushed into this VM so it is released at destroy (a cfunc proc's env
|
|
// has no per-value finalizer to release it when the proc is collected).
|
|
static int32_t mrubyTrackForeign(CalogMrubyT *context, CalogFnT *callable) {
|
|
if (context->foreignCount == context->foreignCap) {
|
|
int32_t newCap;
|
|
CalogFnT **resized;
|
|
newCap = (context->foreignCap == 0) ? MRUBY_FOREIGN_INITIAL : context->foreignCap * CALOG_GROWTH_FACTOR;
|
|
resized = (CalogFnT **)realloc(context->foreignFns, (size_t)newCap * sizeof(CalogFnT *));
|
|
if (resized == NULL) {
|
|
return calogErrOomE;
|
|
}
|
|
context->foreignFns = resized;
|
|
context->foreignCap = newCap;
|
|
}
|
|
context->foreignFns[context->foreignCount] = callable;
|
|
context->foreignCount++;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static mrb_value mrubyTrampoline(mrb_state *mrb, mrb_value self) {
|
|
CalogMrubyT *context;
|
|
const char *name;
|
|
mrb_value *argv;
|
|
mrb_int argc;
|
|
CalogValueT *args;
|
|
CalogValueT result;
|
|
mrb_value out;
|
|
int arena;
|
|
mrb_int index;
|
|
int32_t status;
|
|
|
|
(void)self;
|
|
context = (CalogMrubyT *)mrb->ud;
|
|
name = mrb_sym_name(mrb, mrb_get_mid(mrb)); // which exposed native was called
|
|
mrb_get_args(mrb, "*", &argv, &argc);
|
|
args = NULL;
|
|
if (argc > 0) {
|
|
args = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT));
|
|
if (args == NULL) {
|
|
mrb_raise(mrb, E_RUNTIME_ERROR, "out of memory marshalling native arguments");
|
|
}
|
|
}
|
|
arena = mrb_gc_arena_save(mrb);
|
|
for (index = 0; index < argc; index++) {
|
|
status = mrubyToValue(context, argv[index], &args[index], 0);
|
|
if (status != calogOkE) {
|
|
mrb_int cleanup;
|
|
for (cleanup = 0; cleanup < index; cleanup++) {
|
|
calogValueFree(&args[cleanup]);
|
|
}
|
|
free(args);
|
|
mrb_raise(mrb, E_TYPE_ERROR, "failed to marshal a native argument");
|
|
}
|
|
}
|
|
status = calogCall(context->broker, name, args, (int32_t)argc, &result);
|
|
for (index = 0; index < argc; index++) {
|
|
calogValueFree(&args[index]);
|
|
}
|
|
free(args);
|
|
if (status != calogOkE) {
|
|
char message[CALOG_ERR_MSG_CAP];
|
|
snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "native call failed");
|
|
calogValueFree(&result);
|
|
mrb_raise(mrb, E_RUNTIME_ERROR, message);
|
|
}
|
|
status = mrubyFromValue(context, &result, &out, 0);
|
|
calogValueFree(&result);
|
|
mrb_gc_arena_restore(mrb, arena);
|
|
if (status != calogOkE) {
|
|
mrb_raise(mrb, E_TYPE_ERROR, "failed to marshal the native result");
|
|
}
|
|
return out;
|
|
}
|