Call Roc from Node

This commit is contained in:
Richard Feldman 2023-03-16 09:48:11 -04:00
parent f80e839061
commit 15e0f416d0
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
9 changed files with 138 additions and 85 deletions

View file

@ -3,4 +3,7 @@ node_modules
hello.js
.vscode
libhello
*.dylib
*.so
*.dll
*.o

View file

@ -1 +1 @@
export function hello(): string;
export function hello(arg: string): string;

View file

@ -2,7 +2,11 @@
"targets": [
{
"target_name": "addon",
"sources": [ "hello.c" ]
"sources": [ "demo.c" ],
"libraries": [
"-lhello",
"-L<(module_root_dir)"
]
}
]
}

View file

@ -7,6 +7,8 @@
#include <unistd.h>
#include <node_api.h>
napi_env napi_global_env;
void *roc_alloc(size_t size, unsigned int alignment) { return malloc(size); }
void *roc_realloc(void *ptr, size_t new_size, size_t old_size,
@ -17,9 +19,12 @@ void *roc_realloc(void *ptr, size_t new_size, size_t old_size,
void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); }
__attribute__((noreturn)) void roc_panic(void *ptr, unsigned int alignment)
void roc_panic(void *ptr, unsigned int alignment)
{
rb_raise(rb_eException, "%s", (char *)ptr);
// WARNING: If roc_panic is called before napi_global_env is set,
// the result will be undefined behavior. So never call any Roc
// functions before setting napi_global_env!
napi_throw_error(napi_global_env, NULL, (char *)ptr);
}
void *roc_memcpy(void *dest, const void *src, size_t n)
@ -99,7 +104,7 @@ struct RocBytes init_rocbytes(uint8_t *bytes, size_t len)
{
struct RocBytes ret;
size_t refcount_size = sizeof(size_t);
uint8_t *new_content = (uint8_t *)roc_alloc(len + refcount_size, alignof(size_t)) + refcount_size;
uint8_t *new_content = (uint8_t *)roc_alloc(len + refcount_size, __alignof__(size_t)) + refcount_size;
memcpy(new_content, bytes, len);
@ -192,61 +197,133 @@ size_t roc_str_len(struct RocStr str)
}
}
extern void roc__mainForHost_1_exposed_generic(struct RocBytes *ret, struct RocBytes *arg);
extern void roc__mainForHost_1_exposed_generic(struct RocStr *ret, struct RocStr *arg);
// Receive a value from Ruby, JSON serialized it and pass it to Roc as a List U8
// (at which point the Roc platform will decode it and crash if it's invalid,
// which roc_panic will translate into a Ruby exception), then get some JSON back from Roc
// - also as a List U8 - and have Ruby JSON.parse it into a plain Ruby value to return.
VALUE call_roc(VALUE self, VALUE rb_arg)
{
// This must be required before the to_json method will exist on String.
rb_require("json");
// Receive a string value from Node and pass it to Roc as a RocStr, then get a RocStr
// back from Roc and convert it into a Node string.
napi_value call_roc(napi_env env, napi_callback_info info) {
napi_status status;
// Turn the given Ruby value into a JSON string.
// TODO should we defensively encode it as UTF-8 first?
VALUE json_arg = rb_funcall(rb_arg, rb_intern("to_json"), 0);
// roc_panic needs a napi_env in order to throw a Node exception, so we provide this
// one globally in case roc_panic gets called during the execution of our Roc function.
//
// According do the docs - https://nodejs.org/api/n-api.html#napi_env -
// it's very important that the napi_env that was passed into "the initial
// native function" is the one that's "passed to any subsequent nested Node-API calls,"
// so we must override this every time we call this function (as opposed to, say,
// setting it once during init).
napi_global_env = env;
struct RocBytes arg = init_rocbytes((uint8_t *)RSTRING_PTR(json_arg), RSTRING_LEN(json_arg));
struct RocBytes ret;
// Get the argument passed to the Node function
size_t argc = 1;
napi_value argv[1];
// Call the Roc function to populate `ret`'s bytes.
roc__mainForHost_1_exposed_generic(&ret, &arg);
status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
// Create a rb_utf8_str from the heap-allocated JSON bytes the Roc function returned.
VALUE returned_json = rb_utf8_str_new((char *)ret.bytes, ret.len);
if (status != napi_ok)
{
return NULL;
}
// Now that we've created our Ruby JSON string, we're no longer referencing the RocBytes.
decref((void *)&ret, alignof(uint8_t *));
napi_value node_arg = argv[0];
return rb_funcall(rb_define_module("JSON"), rb_intern("parse"), 1, returned_json);
}
struct RocStr roc_arg;
void Init_demo()
{
VALUE roc_app = rb_define_module("RocApp");
rb_define_module_function(roc_app, "call_roc", &call_roc, 1);
}
// Write the length of the Node string into `roc_arg_len`
size_t roc_arg_len;
napi_value Method(napi_env env, napi_callback_info args) {
napi_value greeting;
napi_status status;
status = napi_get_value_string_utf8(env, node_arg, NULL, 0, &roc_arg_len);
status = napi_create_string_utf8(env, "World!", NAPI_AUTO_LENGTH, &greeting);
if (status != napi_ok) return NULL;
return greeting;
if (status != napi_ok)
{
return NULL;
}
// Node always writes a null terminator, so we need to keep that in mind.
size_t nul_terminated_str_bytes = roc_arg_len + 1;
// Create a RocStr from the Node string
size_t roc_arg_strlen;
char* roc_arg_bytes;
if (nul_terminated_str_bytes < sizeof(struct RocStr))
{
// If it can fit in a small string, use the string itself as the buffer.
roc_arg_bytes = (char*)&roc_arg;
}
else
{
// It was too big for a small string, so do a heap allocation and write into that.
roc_arg_bytes = (char*)roc_alloc(nul_terminated_str_bytes, __alignof__(char));
}
status = napi_get_value_string_utf8(env, node_arg, roc_arg_bytes, nul_terminated_str_bytes, &roc_arg_strlen);
if (status != napi_ok)
{
return NULL;
}
struct RocStr roc_str_arg = init_rocstr((uint8_t*)roc_arg_bytes, roc_arg_strlen);
// Call the Roc function to populate `roc_ret`'s bytes.
struct RocStr roc_ret;
printf("Calling Roc...\n"); // TODO small string is busted for some reason here
// for(size_t i = 0; i < roc_arg_strlen; i++) {
// printf("%c ", ((char*)&roc_str_arg)[i]);
// }
// printf("\n");
roc__mainForHost_1_exposed_generic(&roc_ret, &roc_str_arg);
printf("Called Roc...\n");
// Create a Node string from the Roc string and return it.
char* roc_str_contents;
if (is_small_str(roc_ret))
{
// In a small string, the string itself contains its contents.
roc_str_contents = (char*)&roc_ret;
}
else
{
roc_str_contents = (char*)roc_ret.bytes;
}
napi_value node_ret;
status = napi_create_string_utf8(env, roc_str_contents, roc_str_len(roc_ret), &node_ret);
if (status != napi_ok)
{
return NULL;
}
return node_ret;
}
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
napi_status status;
napi_value fn;
status = napi_create_function(env, NULL, 0, Method, NULL, &fn);
if (status != napi_ok) return NULL;
status = napi_create_function(env, NULL, 0, call_roc, NULL, &fn);
status = napi_set_named_property(env, exports, "hello", fn);
if (status != napi_ok) return NULL;
return exports;
if (status != napi_ok)
{
return NULL;
}
status = napi_set_named_property(env, exports, "hello", fn);
if (status != napi_ok)
{
return NULL;
}
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

View file

@ -1,24 +0,0 @@
#include <node_api.h>
napi_value Method(napi_env env, napi_callback_info args) {
napi_value greeting;
napi_status status;
status = napi_create_string_utf8(env, "World!", NAPI_AUTO_LENGTH, &greeting);
if (status != napi_ok) return NULL;
return greeting;
}
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, NULL, 0, Method, NULL, &fn);
if (status != napi_ok) return NULL;
status = napi_set_named_property(env, exports, "hello", fn);
if (status != napi_ok) return NULL;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

View file

@ -1,3 +1,3 @@
import { hello } from './build/Release/addon'
console.log("Hello,", hello());
console.log("Roc says the following:", hello("Hello from TypeScript"));

View file

@ -3,11 +3,6 @@ app "libhello"
imports []
provides [main] to pf
main : U64 -> Str
main = \num ->
if num == 0 then
"I need a positive number here!"
else
str = Num.toStr num
"The number was \(str), OH YEAH!!! 🤘🤘"
main : Str -> Str
main = \message ->
"TypeScript said to Roc: \(message)! 🎉"

View file

@ -1,12 +1,10 @@
platform "ruby-interop"
requires {} { main : arg -> ret | arg has Decoding, ret has Encoding }
platform "typescript-interop"
requires {} { main : Str -> Str }
exposes []
packages {}
imports [Json]
provides [mainForHost]
mainForHost : List U8 -> List U8
mainForHost = \json ->
when Decode.fromBytes json Json.fromUtf8 is
Ok arg -> Encode.toBytes (main arg) Json.toUtf8
Err _ -> [] # TODO panic so that Ruby raises an exception
mainForHost : Str -> Str
mainForHost = \message ->
main message