Add roc code to ts-interop example

This commit is contained in:
Richard Feldman 2023-03-16 08:31:17 -04:00
parent 7ce4d3b22f
commit ca900550a2
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
3 changed files with 292 additions and 6 deletions

View file

@ -1,10 +1,13 @@
## Running the example # TypeScript Interop
This is an example of calling Roc code from [TypeScript](https://www.typescriptlang.org/) on [Node.js](https://nodejs.org/en/).
## Installation
You'll need to have a C compiler installed, but most operating systems will have one already. You'll need to have a C compiler installed, but most operating systems will have one already.
(e.g. macOS has `clang` installed by default, Linux usually has GCC by default, etc.) (e.g. macOS has `clang` installed by default, Linux usually has GCC by default, etc.)
All of these commands should be run from the same directory as this README file. All of these commands should be run from the same directory as this README file.
### Setup before first build
First, run this to install Node dependencies and generate the Makefile that will be First, run this to install Node dependencies and generate the Makefile that will be
used by future commands. (You should only need to run this once.) used by future commands. (You should only need to run this once.)
@ -14,7 +17,15 @@ npm install
npx node-gyp configure npx node-gyp configure
``` ```
### Build ## Building the Roc library
First, `cd` into this directory and run this in your terminal:
```
roc build --lib
```
This compiles your Roc code into a binary library in the current directory. The library's filename will be libhello plus an OS-specific extension (e.g. libhello.dylib on macOS).
Next, run this to rebuild the C sources. Next, run this to rebuild the C sources.
@ -34,10 +45,20 @@ You can verify that TypeScript sees the correct types with:
npx tsc hello.ts npx tsc hello.ts
``` ```
### Run ### Try it out!
Now you should be able to run the example with: Now that everything is built, you should be able to run the example with:
``` ```
npx ts-node hello.ts npx ts-node hello.ts
``` ```
To rebuild after changing either the `demo.`c file or any `.roc` files, run:
```
roc build --lib && npx node-gyp build
```
## About this example
This was created by following the [NodeJS addons](https://nodejs.org/dist/latest/docs/api/addons.html) tutorial and switching from C++ to C, then creating the `addon.d.ts` file to add types to the generated native Node module.

View file

@ -0,0 +1,252 @@
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <unistd.h>
#include <node_api.h>
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,
unsigned int alignment)
{
return realloc(ptr, new_size);
}
void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); }
__attribute__((noreturn)) void roc_panic(void *ptr, unsigned int alignment)
{
rb_raise(rb_eException, "%s", (char *)ptr);
}
void *roc_memcpy(void *dest, const void *src, size_t n)
{
return memcpy(dest, src, n);
}
void *roc_memset(void *str, int c, size_t n) { return memset(str, c, n); }
// Reference counting
// If the refcount is set to this, that means the allocation is
// stored in readonly memory in the binary, and we must not
// attempt to increment or decrement it; if we do, we'll segfault!
const ssize_t REFCOUNT_READONLY = 0;
const ssize_t REFCOUNT_ONE = (ssize_t)PTRDIFF_MIN;
const size_t MASK = (size_t)PTRDIFF_MIN;
// Increment reference count, given a pointer to the first element in a collection.
// We don't need to check for overflow because in order to overflow a usize worth of refcounts,
// you'd need to somehow have more pointers in memory than the OS's virtual address space can hold.
void incref(uint8_t* bytes, uint32_t alignment)
{
ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1;
ssize_t refcount = *refcount_ptr;
if (refcount != REFCOUNT_READONLY) {
*refcount_ptr = refcount + 1;
}
}
// Decrement reference count, given a pointer to the first element in a collection.
// Then call roc_dealloc if nothing is referencing this collection anymore.
void decref(uint8_t* bytes, uint32_t alignment)
{
if (bytes == NULL) {
return;
}
size_t extra_bytes = (sizeof(size_t) >= (size_t)alignment) ? sizeof(size_t) : (size_t)alignment;
ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1;
ssize_t refcount = *refcount_ptr;
if (refcount != REFCOUNT_READONLY) {
*refcount_ptr = refcount - 1;
if (refcount == REFCOUNT_ONE) {
void *original_allocation = (void *)(refcount_ptr - (extra_bytes - sizeof(size_t)));
roc_dealloc(original_allocation, alignment);
}
}
}
// RocBytes (List U8)
struct RocBytes
{
uint8_t *bytes;
size_t len;
size_t capacity;
};
struct RocBytes init_rocbytes(uint8_t *bytes, size_t len)
{
if (len == 0)
{
struct RocBytes ret = {
.len = 0,
.bytes = NULL,
.capacity = MASK,
};
return ret;
}
else
{
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;
memcpy(new_content, bytes, len);
ret.bytes = new_content;
ret.len = len;
ret.capacity = len;
return ret;
}
}
// RocStr
struct RocStr
{
uint8_t *bytes;
size_t len;
size_t capacity;
};
struct RocStr init_rocstr(uint8_t *bytes, size_t len)
{
if (len == 0)
{
struct RocStr ret = {
.len = 0,
.bytes = NULL,
.capacity = MASK,
};
return ret;
}
else if (len < sizeof(struct RocStr))
{
// Start out with zeroed memory, so that
// if we end up comparing two small RocStr values
// for equality, we won't risk memory garbage resulting
// in two equal strings appearing unequal.
struct RocStr ret = {
.len = 0,
.bytes = NULL,
.capacity = MASK,
};
// Copy the bytes into the stack allocation
memcpy(&ret, bytes, len);
// Record the string's length in the last byte of the stack allocation
((uint8_t *)&ret)[sizeof(struct RocStr) - 1] = (uint8_t)len | 0b10000000;
return ret;
}
else
{
// A large RocStr is the same as a List U8 (aka RocBytes) in memory.
struct RocBytes roc_bytes = init_rocbytes(bytes, len);
struct RocStr ret = {
.len = roc_bytes.len,
.bytes = roc_bytes.bytes,
.capacity = roc_bytes.capacity,
};
return ret;
}
}
bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; }
// Determine the length of the string, taking into
// account the small string optimization
size_t roc_str_len(struct RocStr str)
{
uint8_t *bytes = (uint8_t *)&str;
uint8_t last_byte = bytes[sizeof(str) - 1];
uint8_t last_byte_xored = last_byte ^ 0b10000000;
size_t small_len = (size_t)(last_byte_xored);
size_t big_len = str.len;
// Avoid branch misprediction costs by always
// determining both small_len and big_len,
// so this compiles to a cmov instruction.
if (is_small_str(str))
{
return small_len;
}
else
{
return big_len;
}
}
extern void roc__mainForHost_1_exposed_generic(struct RocBytes *ret, struct RocBytes *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");
// 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);
struct RocBytes arg = init_rocbytes((uint8_t *)RSTRING_PTR(json_arg), RSTRING_LEN(json_arg));
struct RocBytes ret;
// Call the Roc function to populate `ret`'s bytes.
roc__mainForHost_1_exposed_generic(&ret, &arg);
// 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);
// Now that we've created our Ruby JSON string, we're no longer referencing the RocBytes.
decref((void *)&ret, alignof(uint8_t *));
return rb_funcall(rb_define_module("JSON"), rb_intern("parse"), 1, returned_json);
}
void Init_demo()
{
VALUE roc_app = rb_define_module("RocApp");
rb_define_module_function(roc_app, "call_roc", &call_roc, 1);
}
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

@ -0,0 +1,13 @@
app "libhello"
packages { pf: "platform/main.roc" }
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!!! 🤘🤘"