Skip to content

Commit

Permalink
Merge pull request #18 from andrjohns/sexp-to-jsvalue
Browse files Browse the repository at this point in the history
Tidy functionality and add tests for R to JS conversions
  • Loading branch information
andrjohns authored May 13, 2024
2 parents 4b81bf5 + db74159 commit 2cba88c
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 65 deletions.
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
^src/quickjs/\.github$
^src/quickjs/\.git$
semicolon_delimited_script
Makefile
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
clean:
$(MAKE) -C src -f Makevars clean
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export(JSContext)
export(cxxflags)
export(ldflags)
export(qjs_eval)
export(qjs_passthrough)
export(quickjs_version)
importFrom(jsonlite,fromJSON)
useDynLib(QuickJSR, .registration = TRUE)
17 changes: 15 additions & 2 deletions R/qjs.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ qjs_validate <- function(ctx_ptr, function_name) {
.Call(`qjs_validate_`, ctx_ptr, function_name)
}

qjs_passthrough <- function(args) {
.Call(`qjs_passthrough_`, args)
#' qjs_passthrough
#'
#' Test function to pass through arguments
#'
#' @param args Args to pass through
#' @param jsonlite Whether to return a JSON string to be parsed by jsonlite
#' @return The input argument unchanged
#'
#' @export
qjs_passthrough <- function(args, jsonlite = TRUE) {
if (isTRUE(jsonlite)) {
parse_return(.Call(`qjs_passthrough_`, args, jsonlite))
} else {
.Call(`qjs_passthrough_`, args, jsonlite)
}
}
8 changes: 8 additions & 0 deletions inst/include/quickjsr.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifndef QUICKJSR_HPP
#define QUICKJSR_HPP

#include <quickjsr/SEXP_to_JSValue.hpp>
#include <quickjsr/JSValue_to_SEXP.hpp>
#include <quickjsr/JSValue_to_JSON.hpp>

#endif
33 changes: 33 additions & 0 deletions inst/include/quickjsr/JSValue_to_JSON.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#ifndef QUICKJSR_JSVALUE_TO_JSON_HPP
#define QUICKJSR_JSVALUE_TO_JSON_HPP

#include <cpp11.hpp>
#include <quickjs-libc.h>

namespace quickjsr {

std::string JS_ValToJSON(JSContext* ctx, JSValue* val) {
JSValue global = JS_GetGlobalObject(ctx);
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
JSValue stringify = JS_GetPropertyStr(ctx, json, "stringify");

JSValue result_js = JS_Call(ctx, stringify, global, 1, val);
std::string result;
if (JS_IsException(result_js)) {
js_std_dump_error(ctx);
result = "Error!";
} else {
result = JS_ToCString(ctx, result_js);
}

JS_FreeValue(ctx, result_js);
JS_FreeValue(ctx, stringify);
JS_FreeValue(ctx, json);
JS_FreeValue(ctx, global);

return result;
}

} // namespace quickjsr

#endif
31 changes: 31 additions & 0 deletions inst/include/quickjsr/JSValue_to_SEXP.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#ifndef QUICKJSR_JSVALUE_TO_SEXP_HPP
#define QUICKJSR_JSVALUE_TO_SEXP_HPP

#include <cpp11.hpp>
#include <quickjs-libc.h>

namespace quickjsr {

SEXP JSValue_to_SEXP_scalar(JSContext* ctx, JSValue val) {
if (JS_IsBool(val)) {
return cpp11::as_sexp(static_cast<bool>(JS_ToBool(ctx, val)));
}
if (JS_IsNumber(val)) {
double res;
JS_ToFloat64(ctx, &res, val);
return cpp11::as_sexp(res);
}
if (JS_IsString(val)) {
return cpp11::as_sexp(JS_ToCString(ctx, val));
}
return cpp11::as_sexp("Unsupported type");
}

SEXP JSValue_to_SEXP(JSContext* ctx, JSValue val) {
// TODO: Implement array and object conversion
return JSValue_to_SEXP_scalar(ctx, val);
}

} // namespace quickjsr

#endif
66 changes: 66 additions & 0 deletions inst/include/quickjsr/SEXP_to_JSValue.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#ifndef QUICKJSR_SEXP_TO_JSVALUE_HPP
#define QUICKJSR_SEXP_TO_JSVALUE_HPP

#include <cpp11.hpp>
#include <quickjs-libc.h>

namespace quickjsr {
// Forward declaration to allow for recursive calls
JSValue SEXP_to_JSValue(JSContext* ctx, SEXP x, bool auto_unbox);

JSValue SEXP_to_JSValue_elem(JSContext* ctx, SEXP x, int i, bool auto_unbox) {
switch(TYPEOF(x)) {
case REALSXP:
return JS_NewFloat64(ctx, REAL(x)[i]);
case INTSXP:
return JS_NewInt32(ctx, INTEGER(x)[i]);
case LGLSXP:
return JS_NewBool(ctx, LOGICAL(x)[i]);
case STRSXP:
return JS_NewString(ctx, CHAR(STRING_ELT(x, i)));
case VECSXP:
return SEXP_to_JSValue(ctx, VECTOR_ELT(x, i), auto_unbox);
default:
return JS_UNDEFINED;
}
}

JSValue SEXP_to_JSValue_array(JSContext* ctx, SEXP x, bool auto_unbox) {
JSValue arr = JS_NewArray(ctx);
for (int i = 0; i < Rf_length(x); i++) {
JSValue val = SEXP_to_JSValue_elem(ctx, x, i, auto_unbox);
JS_SetPropertyUint32(ctx, arr, i, val);
}
return arr;
}

JSValue SEXP_to_JSValue_object(JSContext* ctx, SEXP x, bool auto_unbox) {
JSValue obj = JS_NewObject(ctx);
for (int i = 0; i < Rf_length(x); i++) {
SEXP name = STRING_ELT(Rf_getAttrib(x, R_NamesSymbol), i);
JSValue val = SEXP_to_JSValue_elem(ctx, x, i, auto_unbox);
JS_SetPropertyStr(ctx, obj, CHAR(name), val);
}
return obj;
}

JSValue SEXP_to_JSValue(JSContext* ctx, SEXP x, bool auto_unbox = false) {
// Following jsonlite conventions:
// - R list with names is an object, otherwise an array
if (TYPEOF(x) == VECSXP) {
if (Rf_getAttrib(x, R_NamesSymbol) != R_NilValue) {
return SEXP_to_JSValue_object(ctx, x, auto_unbox);
} else {
return SEXP_to_JSValue_array(ctx, x, auto_unbox);
}
}
if (Rf_length(x) == 1 && auto_unbox) {
return SEXP_to_JSValue_elem(ctx, x, 0, true);
} else {
return SEXP_to_JSValue_array(ctx, x, true);
}
}

} // namespace quickjsr

#endif
29 changes: 29 additions & 0 deletions inst/tinytest/test_data_conversion.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Check conversions from R types to JS types are consistent with jsonlite.
# - The inputs are directly converted from R to JS types using the QuickJS API.
# - The outputs are returned as JSON strings and parsed back to R using jsonlite.
# - If the conversion is consistent, the output should be the same as the input.
expect_eq_jsonlite <- function(x) {
expect_equal(qjs_passthrough(x), jsonlite::fromJSON(jsonlite::toJSON(x)))
}
expect_eq_jsonlite(1)
expect_eq_jsonlite(1:3)
expect_eq_jsonlite(c(1.5, 2.5))

expect_eq_jsonlite("a")
expect_eq_jsonlite(c("a", "b", "c"))

expect_eq_jsonlite(TRUE)
expect_eq_jsonlite(FALSE)
expect_eq_jsonlite(c(TRUE, FALSE))

expect_eq_jsonlite(list(1, 2, 3))
expect_eq_jsonlite(list(a = 1, b = 2, c = 3))
expect_eq_jsonlite(list(a = "d", b = "e", c = "f"))

expect_eq_jsonlite(list(c(1, 2), c(3, 4)))
expect_eq_jsonlite(list(list(1, 2), list(3, 4)))
expect_eq_jsonlite(list(list(a = 1, b = 2), list(c = 3, d = 4)))

expect_eq_jsonlite(list(c("e", "f"), c("g", "h")))
expect_eq_jsonlite(list(list("e", "f"), list("g", "h")))
expect_eq_jsonlite(list(list(a = "e", b = "f"), list(c = "g", d = "h")))
19 changes: 19 additions & 0 deletions man/qjs_passthrough.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion src/Makevars
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ PKG_CPPFLAGS += -D_GNU_SOURCE -DCONFIG_BIGNUM
PKG_CPPFLAGS += -DCONFIG_VERSION=\"$(shell cat quickjs/VERSION)\"
PKG_LIBS = ../inst/lib/$(R_ARCH)/libquickjs.a

ifeq ($(OS),Windows_NT)
DLL := .dll
else
DLL := .so
endif

CC_VERSION := $(shell $(CC) -dumpfullversion -dumpversion 2>&1)
CC_MAJOR := $(word 1,$(subst ., ,$(CC_VERSION)))

Expand Down Expand Up @@ -54,5 +60,5 @@ $(QUICKJS_OBJECTS): quickjs/%.o : quickjs/%.c
$(CC) $(ALL_CPPFLAGS) $(ALL_CFLAGS) -DSTRICT_R_HEADERS -funsigned-char -fwrapv -std=c11 -c $< -o $@

clean:
$(RM) $(QUICKJS_OBJECTS) $(OBJECTS) ../inst/VERSION
$(RM) $(QUICKJS_OBJECTS) $(OBJECTS) ../inst/VERSION QuickJSR$(DLL)
$(RM) -r ../inst/lib ../inst/include/quickjs
4 changes: 2 additions & 2 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extern "C" {
SEXP qjs_validate_(SEXP ctx_ptr_, SEXP code_string_);
SEXP qjs_call_(SEXP ctx_ptr_, SEXP function_name_, SEXP args_json_);
SEXP qjs_eval_(SEXP eval_string_);
SEXP qjs_passthrough_(SEXP args_);
SEXP qjs_passthrough_(SEXP args_, SEXP jsonlite_rtn_);


static const R_CallMethodDef CallEntries[] = {
Expand All @@ -19,7 +19,7 @@ extern "C" {
{"qjs_eval_", (DL_FUNC) &qjs_eval_, 1},
{"qjs_source_", (DL_FUNC) &qjs_source_, 2},
{"qjs_validate_", (DL_FUNC) &qjs_validate_, 2},
{"qjs_passthrough_", (DL_FUNC) &qjs_passthrough_, 1},
{"qjs_passthrough_", (DL_FUNC) &qjs_passthrough_, 2},
{NULL, NULL, 0}
};

Expand Down
68 changes: 8 additions & 60 deletions src/quickjsr.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <cpp11.hpp>
#include <cpp11/declarations.hpp>
#include <quickjs-libc.h>
#include <quickjsr.hpp>

// Register the cpp11 external pointer types with the correct cleanup/finaliser functions
using ContextXPtr = cpp11::external_pointer<JSContext, JS_FreeContext>;
Expand Down Expand Up @@ -141,64 +142,7 @@ extern "C" SEXP qjs_eval_(SEXP eval_string_) {
END_CPP11
}

JSValue SEXP_to_JSValue_scalar(JSContext* ctx, SEXP x, int i = 0) {
switch(TYPEOF(x)) {
case REALSXP:
return JS_NewFloat64(ctx, REAL(x)[i]);
case INTSXP:
return JS_NewInt32(ctx, INTEGER(x)[i]);
case LGLSXP:
return JS_NewBool(ctx, LOGICAL(x)[i]);
case STRSXP:
return JS_NewString(ctx, CHAR(STRING_ELT(x, i)));
default:
return JS_UNDEFINED;
}
}

JSValue SEXP_to_JSValue(JSContext* ctx, SEXP x) {
if (TYPEOF(x) == VECSXP) {
JSValue obj = JS_NewObject(ctx);
for (int i = 0; i < Rf_length(x); i++) {
SEXP name = STRING_ELT(Rf_getAttrib(x, R_NamesSymbol), i);
JSValue val = SEXP_to_JSValue(ctx, VECTOR_ELT(x, i));
JS_SetPropertyStr(ctx, obj, CHAR(name), val);
}
return obj;
}
if (Rf_length(x) == 1) {
return SEXP_to_JSValue_scalar(ctx, x);
} else {
JSValue arr = JS_NewArray(ctx);
for (int i = 0; i < Rf_length(x); i++) {
JSValue val = SEXP_to_JSValue_scalar(ctx, x, i);
JS_SetPropertyUint32(ctx, arr, i, val);
}
return arr;
}
}

SEXP JSValue_to_SEXP_scalar(JSContext* ctx, JSValue val) {
if (JS_IsBool(val)) {
return cpp11::as_sexp(static_cast<bool>(JS_ToBool(ctx, val)));
}
if (JS_IsNumber(val)) {
double res;
JS_ToFloat64(ctx, &res, val);
return cpp11::as_sexp(res);
}
if (JS_IsString(val)) {
return cpp11::as_sexp(JS_ToCString(ctx, val));
}
return cpp11::as_sexp("Unsupported type");
}

SEXP JSValue_to_SEXP(JSContext* ctx, JSValue val) {
// TODO: Implement array and object conversion
return JSValue_to_SEXP_scalar(ctx, val);
}

extern "C" SEXP qjs_passthrough_(SEXP args_) {
extern "C" SEXP qjs_passthrough_(SEXP args_, SEXP jsonlite_rtn_) {
BEGIN_CPP11
JSRuntime* rt = JS_NewRuntime();
JSContext* ctx = JS_NewContext(rt);
Expand All @@ -214,15 +158,19 @@ extern "C" SEXP qjs_passthrough_(SEXP args_) {
std::string wrapped_name = "passthrough";
JSValue global = JS_GetGlobalObject(ctx);
JSValue function_wrapper = JS_GetPropertyStr(ctx, global, wrapped_name.c_str());
JSValue args[] = { SEXP_to_JSValue(ctx, args_) };
JSValue args[] = { quickjsr::SEXP_to_JSValue(ctx, args_) };
JSValue result_js = JS_Call(ctx, function_wrapper, global, 1, args);

SEXP result;
if (JS_IsException(result_js)) {
js_std_dump_error(ctx);
result = cpp11::as_sexp("Error!");
} else {
result = JSValue_to_SEXP(ctx, result_js);
if (cpp11::as_cpp<bool>(jsonlite_rtn_)) {
result = cpp11::as_sexp(JS_ValToJSON(ctx, &result_js));
} else {
result = quickjsr::JSValue_to_SEXP(ctx, result_js);
}
}

JS_FreeValue(ctx, result_js);
Expand Down

0 comments on commit 2cba88c

Please sign in to comment.