diff --git a/examples/typescript-interop/README.md b/examples/typescript-interop/README.md index 0697c6a65d..a2c0d8e451 100644 --- a/examples/typescript-interop/README.md +++ b/examples/typescript-interop/README.md @@ -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. (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. -### Setup before first build 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.) @@ -14,7 +17,15 @@ npm install 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. @@ -34,10 +45,20 @@ You can verify that TypeScript sees the correct types with: 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 -``` \ No newline at end of file +``` + +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. \ No newline at end of file diff --git a/examples/typescript-interop/demo.c b/examples/typescript-interop/demo.c new file mode 100644 index 0000000000..d305a1e826 --- /dev/null +++ b/examples/typescript-interop/demo.c @@ -0,0 +1,252 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +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) \ No newline at end of file diff --git a/examples/typescript-interop/main.roc b/examples/typescript-interop/main.roc new file mode 100644 index 0000000000..311d89a74c --- /dev/null +++ b/examples/typescript-interop/main.roc @@ -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!!! 🤘🤘" \ No newline at end of file