afb-api-js: first step for javascript bindings
[src/app-framework-binder.git] / src / afb-api-js.c
diff --git a/src/afb-api-js.c b/src/afb-api-js.c
new file mode 100644 (file)
index 0000000..67532aa
--- /dev/null
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2016, 2017 "IoT.bzh"
+ * Author José Bollo <jose.bollo@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <json-c/json.h>
+#include "duktape.h"
+
+#include "afb-common.h"
+#include "afb-api.h"
+#include "afb-apiset.h"
+#include "afb-api-js.h"
+#include "afb-xreq.h"
+#include "jobs.h"
+#include "verbose.h"
+
+/********************************************************************/
+struct jsapi
+{
+       int logmask;
+       duk_context *context;
+       char api[1];
+};
+
+/********************************************************************/
+static void jsapi_call(void *closure, struct afb_xreq *xreq);
+static int jsapi_service_start(void *closure);
+static int jsapi_get_logmask(void *closure);
+static void jsapi_set_logmask(void *closure, int level);
+static struct json_object *jsapi_describe(void *closure);
+
+static struct afb_api_itf jsapi_itf =
+{
+       .call = jsapi_call,
+       .service_start = jsapi_service_start,
+       .set_logmask = jsapi_set_logmask,
+       .get_logmask = jsapi_get_logmask,
+       .describe = jsapi_describe
+};
+
+/********************************************************************/
+static duk_ret_t do_success(duk_context *ctx);
+static duk_ret_t do_fail(duk_context *ctx);
+static duk_ret_t do_subcall_sync(duk_context *ctx);
+static duk_ret_t do_error(duk_context *ctx);
+static duk_ret_t do_require(duk_context *ctx);
+
+static const duk_function_list_entry funcs[] =
+{
+       { "afb_req_success", do_success, 3 },
+       { "afb_req_fail", do_fail, 3 },
+       { "afb_req_subcall_sync", do_subcall_sync, 4 },
+       { "afb_error", do_error, 1 },
+       { "require", do_require, 1 },
+       { NULL, NULL, 0 }
+};
+
+/********************************************************************/
+
+static void on_heap_fatal(void *udata, const char *msg)
+{
+       ERROR("Got fatal from duktape: %s", msg);
+       abort();
+}
+
+static int jsapi_load(duk_context *ctx, const char *path)
+{
+       static const char prefix[] = "function(exports){";
+       static const char suffix[] = "}";
+
+       int fd, rc;
+       struct stat st;
+       char *buffer;
+       ssize_t s;
+
+       fd = afb_common_rootdir_open_locale(path, O_RDONLY, NULL);
+       if (fd < 0) {
+               fd = open(path, O_RDONLY);
+               if (fd < 0) {
+                       ERROR("Can't open %s: %m", path);
+                       duk_push_error_object(ctx, DUK_ERR_ERROR, "Can't open file %s: %m", path);
+                       goto error;
+               }
+       }
+
+       rc = fstat(fd, &st);
+       if (rc < 0) {
+               goto error2;
+       }
+
+       buffer = alloca(st.st_size + sizeof prefix + sizeof suffix);
+       s = read(fd, &buffer[sizeof prefix - 1], st.st_size);
+       if (s < 0)
+               goto error2;
+       if (s != st.st_size)
+               goto error2;
+
+       memcpy(buffer, prefix, sizeof prefix - 1);
+       memcpy(&buffer[sizeof prefix - 1 + st.st_size], suffix, sizeof suffix);
+       close(fd);
+
+       duk_push_object(ctx); /* exports */
+       duk_push_string(ctx, path); /* exports path */
+       rc = duk_pcompile_string_filename(ctx, DUK_COMPILE_FUNCTION|DUK_COMPILE_STRICT, buffer); /* exports func */
+       if (rc) {
+               duk_dup_top(ctx); /* exports error error */
+               ERROR("compiling of %s failed: %s", path, duk_safe_to_string(ctx, -1)); /* exports error error */
+               duk_pop(ctx); /* exports error */
+               duk_replace(ctx, -2); /* error */
+               goto error;
+       }
+       duk_dup(ctx, -2); /* exports func exports */
+       rc = duk_pcall(ctx, 1); /* exports ret */
+       if (rc) {
+               duk_dup_top(ctx); /* exports error error */
+               if (!duk_is_error(ctx, -1)) {
+                       duk_push_error_object(ctx, DUK_ERR_ERROR, "%s", duk_safe_to_string(ctx, -1)); /* exports error error error */
+                       duk_replace(ctx, -3); /* exports error error */
+               }
+               ERROR("initialisation of %s failed: %s", path, duk_safe_to_string(ctx, -1)); /* exports error */
+               duk_pop(ctx); /* exports error */
+               duk_replace(ctx, -2); /* error */
+               goto error;
+       }
+       duk_pop(ctx); /* exports */
+       return 1;
+error2:
+       ERROR("can't process file %s: %m", path);
+       duk_push_error_object(ctx, DUK_ERR_ERROR, "Can't process file %s: %m", path);
+       close(fd);
+error:
+       return duk_throw(ctx);
+}
+
+static duk_ret_t do_require(duk_context *ctx)
+{
+       int rc;
+       const char *path;
+
+       path = duk_require_string(ctx, -1); /* path */
+       duk_push_global_stash(ctx); /* path gstash */
+       duk_dup(ctx, -2); /* path gstash path */
+       rc = duk_get_prop(ctx, -2); /* path gstash ? */
+       if (!rc) {
+               /* path gstash error (ELSE: path gstash exports) */
+               duk_pop(ctx); /* path gstash */
+               rc = jsapi_load(ctx, path); /* path gstash ? */
+               if (rc > 0) {
+                       /* path gstash exports (ELSE: path gstash error) */
+                       duk_dup_top(ctx); /* path gstash exports exports */
+                       duk_swap(ctx, -2, -4); /* exports gstash path exports */
+                       duk_put_prop(ctx, -3); /* exports gstash */
+                       duk_pop(ctx); /* exports */
+               }
+       }
+       return 1;
+}
+
+static void jsapi_destroy(struct jsapi *jsapi)
+{
+       duk_destroy_heap(jsapi->context);
+       free(jsapi);
+}
+
+static struct jsapi *jsapi_create(const char *path)
+{
+       struct jsapi *jsapi;
+       duk_context *ctx;
+       const char *api, *ext;
+
+       /* allocate and initialise names */
+       api = strrchr(path, '/');
+       api = api ? api + 1 : path;
+       ext = strrchr(api, '.') ?: &api[strlen(api)];
+       jsapi = malloc(sizeof *jsapi + 1 + (ext - api));
+       if (!jsapi)
+               goto error;
+       memcpy(jsapi->api, api, ext - api);
+       jsapi->api[ext - api] = 0;
+       jsapi->logmask = logmask;
+
+       /* create the duktape context */
+       ctx = duk_create_heap(NULL, NULL, NULL, NULL, on_heap_fatal);
+       if (!ctx)
+               goto error;
+
+       jsapi->context = ctx;
+
+       /* populate global with functions */
+       duk_push_global_object(ctx);
+       duk_put_function_list(ctx, -1, funcs);
+       duk_pop(ctx);
+
+       /* call the require path */
+       ctx = jsapi->context;
+       duk_get_global_string(ctx, "require");
+       duk_push_string(ctx, path);
+       duk_pcall(ctx, 1);
+       if (duk_is_error(ctx, -1)) {
+               const char *message, *file, *stack;
+               int line;
+               duk_get_prop_string(ctx, -1, "message");
+               message = duk_get_string(ctx, -1);
+               duk_get_prop_string(ctx, -2, "fileName");
+               file = duk_get_string(ctx, -1);
+               duk_get_prop_string(ctx, -3, "lineNumber");
+               line = (int)duk_get_int(ctx, -1);
+               duk_get_prop_string(ctx, -4, "stack");
+               stack = duk_get_string(ctx, -1);
+               ERROR("Initialisation of API %s from jsapi %s failed file %s (file %s, line %d) stack:\n%s\n", jsapi->api, path, message, file, line, stack);
+               jsapi_destroy(jsapi);
+               return NULL;
+       }
+       duk_put_global_string(ctx, "exports");
+       return jsapi;
+
+error:
+       ERROR("out of memory");
+       free(jsapi);
+       errno = ENOMEM;
+       return NULL;
+}
+
+int afb_api_js_add(const char *path, struct afb_apiset *declare_set, struct afb_apiset* call_set)
+{
+       int rc;
+       struct jsapi *jsapi;
+       struct afb_api_item api;
+
+       jsapi = jsapi_create(path);
+       if (!jsapi)
+               goto error;
+
+       api.closure = jsapi;
+       api.itf = &jsapi_itf;
+       api.group = jsapi;
+       rc = afb_apiset_add(declare_set, jsapi->api, api);
+       if (!rc)
+               return 0;
+
+       duk_destroy_heap(jsapi->context);
+       free(jsapi);
+error:
+       return -1;
+}
+
+/********************************************************************/
+
+static duk_ret_t do_success(duk_context *ctx)
+{
+       struct afb_xreq *xreq;
+       const char *json, *info;
+
+       xreq = duk_get_pointer(ctx, -3);
+       duk_json_encode(ctx, -2);
+       json = duk_get_string(ctx, -2);
+       info = duk_get_string(ctx, -1);
+
+       afb_xreq_reply(xreq, json ? json_tokener_parse(json) : NULL, NULL, info);
+       return 0;
+}
+
+static duk_ret_t do_fail(duk_context *ctx)
+{
+       struct afb_xreq *xreq;
+       const char *status, *info;
+
+       xreq = duk_get_pointer(ctx, -3);
+       status = duk_get_string(ctx, -2);
+       info = duk_get_string(ctx, -1);
+
+       afb_xreq_reply(xreq, NULL, status ?: "error", info);
+       return 0;
+}
+
+static duk_ret_t do_subcall_sync(duk_context *ctx)
+{
+       int rc;
+       struct afb_xreq *xreq;
+       const char *api, *verb, *json;
+       struct json_object *resu;
+
+       xreq = duk_get_pointer(ctx, -4);
+       api = duk_get_string(ctx, -3);
+       verb = duk_get_string(ctx, -2);
+       duk_json_decode(ctx, -1);
+       json = duk_get_string(ctx, -1);
+
+       resu = NULL;
+       rc = afb_xreq_legacy_subcall_sync(xreq, api, verb, json ? json_tokener_parse(json) : NULL, &resu);
+       if (rc)
+               duk_push_null(ctx);
+       else {
+               duk_push_string(ctx, json_object_to_json_string(resu));
+               duk_json_decode(ctx, -1);
+       }
+       json_object_put(resu);
+       return 1;
+}
+
+static duk_ret_t do_error(duk_context *ctx)
+{
+       const char *message;
+
+       message = duk_get_string(ctx, -1);
+
+       ERROR("%s", message ? : "null");
+       return 0;
+}
+
+/********************************************************************/
+
+static void jsapi_call(void *closure, struct afb_xreq *xreq)
+{
+       duk_idx_t top;
+       duk_context *ctx;
+       json_object *args;
+       const char *json;
+       struct jsapi *jsapi = closure;
+
+       ctx = jsapi->context;
+       top = duk_get_top(ctx);
+       duk_get_global_string(ctx, "exports");
+       if (!duk_is_object(ctx, -1)) {
+               afb_xreq_reply(xreq, NULL, "internal-error", "no exports!?");
+               goto end;
+       }
+       duk_get_prop_string(ctx, -1, xreq->request.called_verb);
+       if (!duk_is_function(ctx, -1)) {
+               afb_xreq_reply_unknown_verb(xreq);
+               goto end;
+       }
+       duk_push_pointer(ctx, xreq);
+       args = afb_xreq_json(xreq);
+       json = json_object_to_json_string(args);
+       duk_push_string(ctx, json);
+       duk_json_decode(ctx, -1);
+       duk_pcall(ctx, 2);
+end:
+       duk_pop_n(ctx, duk_get_top(ctx) - top);
+}
+
+static int jsapi_service_start(void *closure)
+{
+       struct jsapi *jsapi = closure;
+       return 0;
+}
+
+static int jsapi_get_logmask(void *closure)
+{
+       struct jsapi *jsapi = closure;
+       return jsapi->logmask;
+}
+
+static void jsapi_set_logmask(void *closure, int level)
+{
+       struct jsapi *jsapi = closure;
+       jsapi->logmask = level;
+}
+
+static struct json_object *jsapi_describe(void *closure)
+{
+       struct jsapi *jsapi = closure;
+       return NULL;
+}
+