├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package.json ├── src ├── Generator.ts ├── Log.ts ├── Parser.ts ├── generators │ └── DefaultNan │ │ ├── DefaultNanGenerator.ts │ │ ├── addonTemplate.ts │ │ └── templates.ts ├── interfaces │ └── IABIInterface.ts └── runo-bridge.ts ├── test ├── generator_test.js ├── integration │ └── test.js ├── parser_test.js └── resources │ ├── Cargo.toml │ ├── README.md │ ├── binding.gyp │ ├── build-scripts │ ├── clean.js │ └── install-rust.js │ ├── input.json │ ├── int_to_int.rs │ ├── package.json │ └── src │ └── embed.rs ├── tsconfig.json └── typings └── tsd.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | test/resources/target 5 | test/resources/node_modules 6 | test/resources/build 7 | test/resources/Cargo.lock 8 | test/resources/src/addon.cc 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | test/resources/target 4 | test/resources/node_modules 5 | test/resources/build 6 | test/resources/Cargo.lock 7 | test/resources/src/addon.cc 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | os: 4 | - linux 5 | 6 | rust: 7 | - stable 8 | - beta 9 | - nightly 10 | 11 | env: 12 | - CXX=g++-4.8 13 | 14 | addons: 15 | apt: 16 | sources: 17 | - ubuntu-toolchain-r-test 18 | packages: 19 | - g++-4.8 20 | 21 | matrix: 22 | allow_failures: 23 | - rust: nightly 24 | 25 | before_install: 26 | - source $HOME/.nvm/nvm.sh 27 | - nvm install 4 28 | - nvm use 4 29 | - npm install 30 | - npm test 31 | - nvm install 5 32 | - nvm use 5 33 | 34 | script: 35 | - npm run test-full 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is archived 2 | 3 | # RuNo bridge Simple Rust to NodeJS bridge 4 | 5 | **A Prototype! Please read whole readme first and use on your own risk!** 6 | 7 | [![Build Status](https://api.travis-ci.org/andruhon/runo-bridge.svg?branch=master)](http://travis-ci.org/andruhon/runo-bridge) 8 | 9 | **Call your Rust dynamic lib from Node JS with native C++ performance** 10 | 11 | Do not hesitate to ask a question or propose something, I've created a special discussion issue for your convenience: https://github.com/andruhon/runo-bridge/issues/1 12 | 13 | RuNo bridge is a command line tool which generates C++ boilerplate addon code to call Rust library from. It is not allmighty and only support primitives such as `int`, `float/double`, `bool` and a `string` at a moment, however it does not require any C++ knowledge from developer if you use primitives mentioned above and your Rust ABI interface complies with simple requirements: 14 | 15 | * All your ABI functoins should be listed in one Rust file; 16 | * Your library should use crate libc; 17 | * Each ABI function should be preceeded with `#[no_mangle]`; 18 | * Each ABI function should be prefixed with `pub extern "C"`; 19 | * ABI Functions should only take params of `c_int`,`c_float`,`c_double` or `*c_char` (as a C string with EOF); 20 | * ABI Functions should return either one of `c_int`,`c_float`,`c_double` or `*c_char` (as a C string with EOF) 21 | 22 | RuNo bridge does not validate or compiles your Rust code, we presume that it is valid and compiles. 23 | 24 | See [embed.rs](test/resources/src/embed.rs) in tests for example of compatible Rust code. Take this file as a source of true for current version. 25 | 26 | See https://github.com/andruhon/runo-bridge-example for more standalone usage example 27 | 28 | ##Important 29 | This package itself does not need Rust or C++ with node-gyp, it just emits a C++ source file. 30 | 31 | However in order to build the source code, your rust and C++ compiler should be compatible with your NodeJS. It is **particularly important on Windows**, where Rust target should be MSVC not GNU, unless you building your NodeJS for Windows from source with GCC. For example, if one using **32 bit** NodeJS on Windows this one should use target `i686-pc-windows-msvc`, if **64 bit** Node then Rust should be configured with `x86_64-pc-windows-msvc` compile target. The same about C++: Everything is mostrly smooth on platforms with GCC, and a bit painful with MS Visual C++, please refer to [node-gyp installation instructions](https://github.com/nodejs/node-gyp) for details. 32 | 33 | ##Installation 34 | 35 | npm install runo-bridge -g 36 | 37 | * global option used here for simplisity, however it is better to install it locally and run it as local npm binary. 38 | 39 | ##Usage 40 | 41 | **Generate V8 addon C++ code from Rust source source.rs** 42 | 43 | runo-bridge generate [options] 44 | 45 | **Options available**: 46 | 47 | --async [NO|ALL|DETECT] - With this option it is possible to make functions async. 48 | You don't need to mangle with V8 callbacks, 49 | RuNo will generate C++ boilerplate code for you, which will run function 50 | in a separate thread, take result from the function and pass value into callback. 51 | Again you just write a normal function and RuNo will do all thread magic for you. 52 | 53 | * NO do not wrap functions into async wrappers (default); 54 | * ALL all functions will be called in a separate thread, adding last param as a callback with result argument; 55 | * DETECT detect functions with 'async' and make them async as described above; 56 | 57 | See double_multiply_plus2_async in [embed.rs](test/resources/src/embed.rs) 58 | and [integration/test.js](test/integration/test.js) for async usage example. 59 | 60 | Example: 61 | 62 | runo-bridge generate src/source.rs intermediates/addon.cc 63 | 64 | RuNo will look for `no-manlge` `extern "C"` functions and will generate NodeJS addon boilerplate for them. 65 | 66 | runo-bridge generate src/source.rs intermediates/addon.cc --async DETECT 67 | 68 | same as above, plus detect functions with 'async' in the name and generate 69 | boilerplate code to run then asynchronously, adding last param as an async 70 | callback with a single param with a function result. 71 | 72 | It is also possible to **provide library binary interface definition as a JSON** 73 | 74 | runo-bridge generate src/my-interface.json intermediates/addon.cc 75 | 76 | See example JSON format in [input.json](test/resources/input.json) . Theoretically you can use this approach with any *precompiled* C ABI compatible dynamic library. 77 | 78 | ##Testing 79 | 80 | Do `npm install` first. 81 | 82 | Test RuNo bridge only: 83 | 84 | npm run test 85 | 86 | Test that generated addon actually works (integration tests): 87 | 88 | npm run test-full 89 | 90 | ##Motivation 91 | 92 | It would be nice to have something simple to use with just implementing extern C method with C primitives without knowledge of V8. FFI seems to be a good option, however, unfortunately it is far to slow to call multiple functions, see: https://github.com/wtfil/rust-in-node#results 93 | 94 | ##Other options 95 | * https://github.com/rustbridge/neon/ Neon bridge (requires knowledge of V8) 96 | * https://github.com/node-ffi/node-ffi Node FFI (good, but slow) 97 | * https://github.com/andruhon/rust-in-node-examples implement your own addon to call Rust via C ABI 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runo-bridge", 3 | "version": "0.2.1", 4 | "description": "Simple Rust to NodeJS bridge", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/andruhon/runo-bridge" 8 | }, 9 | "main": "dist/runo-bridge.js", 10 | "bin": "dist/runo-bridge.js", 11 | "scripts": { 12 | "test": "npm run build && mocha", 13 | "test-full": "npm run build && mocha && npm run integration-test", 14 | "build": "tsc", 15 | "int-install-deps": "cd test/resources && npm install", 16 | "int-generate-cc": "node dist/runo-bridge.js generate test/resources/src/embed.rs test/resources/src/addon.cc --async detect", 17 | "int-build-rust": "cd test/resources && cargo build", 18 | "int-clean": "cd test/resources && node build-scripts/clean.js", 19 | "int-install-rust": "cd test/resources && node build-scripts/install-rust.js", 20 | "int-build-addon": "npm run int-install-rust && cd test/resources && node-gyp configure && node-gyp build", 21 | "int-run": "cd test/integration && mocha", 22 | "integration-test": "npm run int-clean && npm run int-install-deps && npm run integration-test-short", 23 | "integration-test-short": "npm run int-generate-cc && npm run int-build-rust && npm run int-install-rust && npm run int-build-addon && npm run int-run" 24 | }, 25 | "author": "Andrew Kondratev ", 26 | "keywords": [ 27 | "Rust", 28 | "NodeJS" 29 | ], 30 | "license": "ISC", 31 | "devDependencies": { 32 | "fs-extra": "^0.26.5", 33 | "mocha": "^2.4.5", 34 | "retyped-commander-tsd-ambient": "^2.3.0-0", 35 | "retyped-node-tsd-ambient": "^1.5.3-0", 36 | "retyped-prettyjson-tsd-ambient": "0.0.0-0", 37 | "typescript": "^1.8.2" 38 | }, 39 | "dependencies": { 40 | "commander": "^2.9.0", 41 | "prettyjson": "^1.1.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Generator.ts: -------------------------------------------------------------------------------- 1 | import {IInterfaceDefinition, IFunctionDefinition} from './interfaces/IABIInterface'; 2 | import {Log, LOGLEV} from './Log'; 3 | 4 | export const l = new Log(LOGLEV.ERR); 5 | 6 | export abstract class Generator { 7 | 8 | public static UNSUPPORTED_TYPE = "unsupported type"; 9 | public static WARN_FLOAT = "v8 alwayse use c_double internally, c_float might lead to precision loose"; 10 | 11 | protected static noManglePattern = '#[no_mangle]'; 12 | protected static fnDefPattern = 'pub extern "C" fn '; 13 | protected static fnSigPattern = "(\\w+)\\s*\\((.*)\\)\\s*(->)?\\s*((\\*\\w*\\s*)?\\w*)"; 14 | 15 | constructor(protected input: IInterfaceDefinition) {} 16 | 17 | abstract generate(): string; 18 | 19 | public static mapToCType(type: string): string { 20 | switch (type) { 21 | case "*c_char": 22 | return "char *"; 23 | default: 24 | return type.replace("c_",""); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Log.ts: -------------------------------------------------------------------------------- 1 | export enum LOGLEV { 2 | ERR = 1, 3 | INF = 2, 4 | WARN = 3, 5 | DEBUG = 4 6 | } 7 | 8 | export class Log { 9 | 10 | constructor(public level: LOGLEV = LOGLEV.INF) {} 11 | 12 | public err(msg: string) { 13 | if(this.level>=LOGLEV.ERR) console.error(msg); 14 | } 15 | public log(msg: string) { 16 | if(this.level>=LOGLEV.INF) console.log(msg); 17 | } 18 | public warn(msg: string) { 19 | if(this.level>=LOGLEV.WARN) console.warn(msg); 20 | } 21 | public debug(msg: any) { 22 | if(this.level>=LOGLEV.DEBUG) console.log(msg); 23 | } 24 | 25 | public wrapped(msg: any, wrapper: (any)=>any, level: LOGLEV = LOGLEV.INF) { 26 | if(this.level>=level) console.log(wrapper(msg)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Parser.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | import * as readline from 'readline'; 5 | import {Log, LOGLEV} from './Log'; 6 | import * as prettyjson from 'prettyjson'; 7 | 8 | import {IInterfaceDefinition, IFunctionDefinition} from './interfaces/IABIInterface'; 9 | 10 | const l = new Log(); 11 | 12 | export interface IParserSettings { 13 | noManglePattern?: string, 14 | fnDefPattern?: string, 15 | fnSigPattern?: string, 16 | verbosity?: LOGLEV, 17 | async?: ASYNC, 18 | asyncDetectString?: string 19 | } 20 | 21 | export enum ASYNC { 22 | NO, 23 | ALL, 24 | DETECT 25 | } 26 | 27 | export class Parser { 28 | 29 | protected settings = { 30 | noManglePattern: '#[no_mangle]', 31 | fnDefPattern: 'pub extern "C" fn ', 32 | fnSigPattern: '(\\w+)\\s*\\((.*)\\)\\s*(->)?\\s*((\\*\\w*\\s*)?\\w*)', 33 | verbosity: LOGLEV.INF, 34 | async: ASYNC.NO, 35 | asyncDetectString: 'async' //lowercase only for now 36 | } 37 | 38 | constructor(protected source: NodeJS.ReadableStream, protected name: string, settings?: IParserSettings) { 39 | if (settings) Object.assign(this.settings, settings); //mutate settings 40 | l.level = this.settings.verbosity 41 | } 42 | 43 | protected parseFunc (fnDef: string): IFunctionDefinition { 44 | let fnSig = fnDef.substr(this.settings.fnDefPattern.length) 45 | let fnSigRegex = new RegExp(this.settings.fnSigPattern, "g"); 46 | let parsed = fnSigRegex.exec(fnSig); 47 | let async = false; 48 | let output = "void"; 49 | if (!parsed) { 50 | l.err("can't parse "+fnSig); 51 | return; 52 | } 53 | let parameters = parsed[2].split(",").map(function(v){ 54 | let param = v.split(":"); 55 | return {name: param[0].trim(), type: param[1].replace(/(const|mut)*/g,"").replace(/\s*/g,"")}; 56 | }); 57 | if (parsed[4]) { 58 | output = parsed[4].replace(/(const|mut)/,"").replace(/\s*/g,"") 59 | } 60 | switch (this.settings.async) { 61 | case ASYNC.ALL: 62 | async = true; 63 | break; 64 | case ASYNC.DETECT: 65 | if (parsed[1].toLowerCase().indexOf(this.settings.asyncDetectString)>=0) { 66 | async = true; 67 | } 68 | break; 69 | } 70 | return { 71 | name: parsed[1], 72 | parameters: parameters, 73 | return: output, 74 | async: async 75 | } 76 | } 77 | 78 | protected parseInner = (resolve,reject) => { 79 | const rl = readline.createInterface({ 80 | input: this.source 81 | }); 82 | let s = this.settings; 83 | let prevMangle = false; 84 | let results = { 85 | module_name: path.basename(this.name), 86 | functions: [] 87 | }; 88 | rl.on('line', (line) => { 89 | if (prevMangle) { 90 | var fnDef = line.trim().replace(/\s+/g," "); 91 | if (fnDef.startsWith(s.fnDefPattern)) { 92 | var fnParsed = this.parseFunc(fnDef); 93 | if (fnParsed) { 94 | results.functions.push(fnParsed); 95 | } 96 | } else { 97 | l.err(s.noManglePattern+" is not followed by the line with"+s.fnDefPattern); 98 | } 99 | } 100 | if(line.trim().startsWith(s.noManglePattern)) { 101 | prevMangle = true; 102 | } else { 103 | prevMangle = false; 104 | } 105 | }); 106 | rl.on('close',function(){ 107 | l.log("Parse result:"); 108 | l.wrapped(results, prettyjson.render); 109 | resolve(results); 110 | }); 111 | } 112 | 113 | public parse() { 114 | return new Promise(this.parseInner); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/generators/DefaultNan/DefaultNanGenerator.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import {IInterfaceDefinition, IFunctionDefinition} from '../../interfaces/IABIInterface'; 4 | import {Generator, l} from '../../Generator'; 5 | import * as t from './templates'; 6 | import {addonTemplate} from './addonTemplate'; 7 | 8 | export class DefaultNanGenerator extends Generator { 9 | 10 | protected static allowedTypes = [ 11 | /* some primitives from Rust's libc */ 12 | //"c_double", //JS number 13 | "c_int", //JS number 14 | "c_float", //JS float 15 | "c_double", //JS float 16 | "bool", // JS boolean 17 | "void", 18 | 19 | /* pointers */ 20 | "*c_char", //JS String 21 | // "*c_double", //JS number array 22 | // "*int_number", //JS number array 23 | // "**c_char", //JS string array 24 | // 25 | // /** 26 | // * struct with combination of things above 27 | // * (code is parsed, so we know member names and will treat it as a JS object) 28 | // */ 29 | // "struct" //JS object 30 | ]; 31 | 32 | protected extern_c_functions = []; 33 | protected methods = []; 34 | protected inits = []; 35 | 36 | constructor(protected input: IInterfaceDefinition) { 37 | super(input); 38 | } 39 | 40 | protected createExternDefinition (func:IFunctionDefinition): string { 41 | return t.externCFunc(DefaultNanGenerator.allowedTypes, func); 42 | } 43 | 44 | //TODO: check reserved words such as callback, argv and result 45 | protected createMethod (func:IFunctionDefinition): string { 46 | let parameters = []; 47 | let externParams = []; 48 | let deallocations = []; 49 | let v8ReturnValue; 50 | func.parameters.forEach((param, index)=>{ 51 | 52 | switch (param.type) { 53 | case "c_int": 54 | case "c_double": 55 | case "c_float": 56 | case "bool": 57 | parameters.push(t.funcParameterDefault(index,param)); 58 | break; 59 | case "*c_char": 60 | parameters.push(t.funcParameterCString(index,param)); 61 | deallocations.push(t.dealloc(param)); 62 | break; 63 | default: 64 | l.err(param.name + " param "+ param.type); 65 | throw Error(Generator.UNSUPPORTED_TYPE); 66 | } 67 | externParams.push(param.name); 68 | }); 69 | switch(func.return) { 70 | case "c_int": 71 | case "c_double": 72 | case "c_float": 73 | case "bool": 74 | v8ReturnValue = t.returnValueDefault(); 75 | break; 76 | case "*c_char": 77 | v8ReturnValue = t.returnValueCString(); 78 | break; 79 | case "void": 80 | break; 81 | default: 82 | l.err(func.name+" returns "+func.return); 83 | throw Error(Generator.UNSUPPORTED_TYPE); 84 | } 85 | 86 | return t.nanMethod(func, parameters, externParams, deallocations, v8ReturnValue); 87 | } 88 | 89 | protected createInit (func: IFunctionDefinition): string { 90 | return t.init(func); 91 | } 92 | 93 | public generate (): string { 94 | this.input.functions.forEach((func)=>{ 95 | this.extern_c_functions.push(this.createExternDefinition(func)); 96 | this.methods.push(this.createMethod(func)); 97 | this.inits.push(this.createInit(func)); 98 | }); 99 | 100 | return addonTemplate(this.extern_c_functions.join(''), this.methods.join(''), this.inits.join('')); 101 | }; 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/generators/DefaultNan/addonTemplate.ts: -------------------------------------------------------------------------------- 1 | export function addonTemplate(extern_c_functions: string, methods: string, inits: string) { 2 | 3 | return `//Header 4 | //This could go into separate header file defining interface: 5 | #ifndef NATIVE_EXTENSION_GRAB_H 6 | #define NATIVE_EXTENSION_GRAB_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | using namespace std; 16 | using namespace v8; 17 | using v8::Function; 18 | using v8::Local; 19 | using v8::Number; 20 | using v8::Value; 21 | using Nan::AsyncQueueWorker; 22 | using Nan::AsyncWorker; 23 | using Nan::Callback; 24 | using Nan::New; 25 | using Nan::Null; 26 | using Nan::To; 27 | 28 | #endif 29 | 30 | 31 | /* extern interface for Rust functions */ 32 | extern "C" {${extern_c_functions} 33 | } 34 | 35 | ${methods} 36 | 37 | NAN_MODULE_INIT(InitAll) {${inits} 38 | } 39 | 40 | NODE_MODULE(addon, InitAll) 41 | `; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/generators/DefaultNan/templates.ts: -------------------------------------------------------------------------------- 1 | import {IInterfaceDefinition, IFunctionDefinition, IFunctionParemeterDefinition} from '../../interfaces/IABIInterface'; 2 | import {Generator, l} from '../../Generator'; 3 | import * as os from 'os'; 4 | 5 | export function externCFunc (allowedTypes: string[],func: IFunctionDefinition) { 6 | 7 | let parameters = []; 8 | func.parameters.forEach(function(param){ 9 | if (allowedTypes.indexOf(param.type)<0) { 10 | l.err(param.type+" param "+param.type); 11 | throw Error(Generator.UNSUPPORTED_TYPE); 12 | } 13 | parameters.push(Generator.mapToCType(param.type)+" "+param.name) 14 | }); 15 | if (allowedTypes.indexOf(func.return)<0) { 16 | l.err(func.name+" returns "+func.return); 17 | throw Error(Generator.UNSUPPORTED_TYPE); 18 | } 19 | 20 | return ` 21 | extern "C" ${Generator.mapToCType(func.return)} ${func.name}(${parameters.join(", ")});` 22 | } 23 | 24 | export function funcParameterDefault (index: number, param: IFunctionParemeterDefinition) { 25 | let inType = Generator.mapToCType(param.type); 26 | if (param.type=="c_float") { 27 | l.warn(Generator.WARN_FLOAT); 28 | inType = "double"; 29 | } 30 | return ` 31 | ${inType} ${param.name} = To<${inType}>(info[${index}]).FromJust();` 32 | } 33 | 34 | export function funcParameterCString (index: number, param: IFunctionParemeterDefinition) { 35 | return ` 36 | Nan::HandleScope scope; 37 | String::Utf8Value cmd_${param.name}(info[${index}]); 38 | string s_${param.name} = string(*cmd_${param.name}); 39 | char *${param.name} = (char*) malloc (s_${param.name}.length() + 1); 40 | strcpy(${param.name}, s_${param.name}.c_str());` 41 | } 42 | 43 | export function dealloc (param: IFunctionParemeterDefinition) { 44 | return ` 45 | free(${param.name});` 46 | } 47 | 48 | export function returnValueDefault () { 49 | return ` 50 | info.GetReturnValue().Set(result);` 51 | } 52 | 53 | export function returnValueCString () { 54 | return ` 55 | info.GetReturnValue().Set(Nan::New(result).ToLocalChecked()); 56 | free(result);` 57 | } 58 | 59 | export function init (func: IFunctionDefinition) { 60 | return ` 61 | Nan::Set( 62 | target, New("${func.name}").ToLocalChecked(), 63 | Nan::GetFunction(New(${func.name})).ToLocalChecked() 64 | );` 65 | } 66 | 67 | function v8asyncOutput(returnType: string){ 68 | switch (returnType) { 69 | case "c_int": 70 | case "c_double": 71 | case "c_float": 72 | case "bool": 73 | return "Nan::New(result)"; 74 | case "*c_char": 75 | default: 76 | return "Nan::New(result).ToLocalChecked()"; 77 | } 78 | } 79 | 80 | function asyncWorker( 81 | func: IFunctionDefinition 82 | ) { 83 | let workerParams = func.parameters.map((p)=>{ 84 | return {name: p.name, type: Generator.mapToCType(p.type)}; 85 | }); 86 | return ` 87 | class ${func.name}Worker : public AsyncWorker { 88 | public: 89 | ${func.name}Worker(Callback *callback, ${workerParams.map((p)=>`${p.type} ${p.name}`).join(', ')}) 90 | : AsyncWorker(callback), ${workerParams.map((p)=>`${p.name}(${p.name})`).join(', ')} {} 91 | ~${func.name}Worker() {} 92 | 93 | // Executed inside the worker-thread. 94 | // It is not safe to access V8, or V8 data structures 95 | // here, so everything we need for input and output 96 | // should go on 'this'. 97 | void Execute () { 98 | result = ${func.name}(${workerParams.map((p)=>p.name).join(', ')}); 99 | } 100 | 101 | // Executed when the async work is complete 102 | // this function will be run inside the main event loop 103 | // so it is safe to use V8 again 104 | void HandleOKCallback () { 105 | Nan::HandleScope scope; 106 | Local argv[] = { 107 | ${v8asyncOutput(func.return)} 108 | }; 109 | callback->Call(1, argv); 110 | } 111 | 112 | private: 113 | ${workerParams.map((p)=>`${p.type} ${p.name};`).join(os.EOL)} 114 | ${Generator.mapToCType(func.return)} result; 115 | }; 116 | `; 117 | } 118 | 119 | export function nanMethod( 120 | func: IFunctionDefinition, 121 | parameters: string[], 122 | externParams: string[], 123 | deallocations: string[], 124 | v8ReturnValue: string 125 | ) { 126 | let funcCall: string; 127 | if (func.async) { 128 | funcCall = ` 129 | Nan::Callback * callback = new Nan::Callback(info[${externParams.length}].As()); 130 | AsyncQueueWorker(new ${func.name}Worker(callback, ${externParams.join(", ")}));`; 131 | v8ReturnValue = ''; 132 | } else { 133 | funcCall = `${Generator.mapToCType(func.return)} result = ${func.name}(${externParams.join(", ")});`; 134 | } 135 | return ` ${func.async?asyncWorker(func):''} 136 | NAN_METHOD(${func.name}) {`+ 137 | `${parameters.join("")} 138 | ${funcCall}`+ 139 | `${v8ReturnValue}`+ 140 | `${deallocations.join('')} 141 | }`; 142 | } 143 | -------------------------------------------------------------------------------- /src/interfaces/IABIInterface.ts: -------------------------------------------------------------------------------- 1 | export interface IFunctionParemeterDefinition { 2 | name: string, type: string 3 | } 4 | 5 | export interface IFunctionDefinition { 6 | name: string; 7 | parameters: IFunctionParemeterDefinition[]; 8 | return: string; 9 | async?: boolean; 10 | } 11 | 12 | export interface IInterfaceDefinition { 13 | module_name: string; 14 | functions: IFunctionDefinition[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/runo-bridge.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.title = 'runo-bridge'; 4 | 5 | import {Parser, ASYNC, IParserSettings} from "./Parser"; 6 | import * as fs from "fs"; 7 | import * as path from "path"; 8 | import * as program from "commander"; 9 | 10 | 11 | var generatorTemplate = 'DefaultNan'; 12 | var generatorName = generatorTemplate+'Generator'; 13 | var generator = require("../dist/generators/"+generatorTemplate+"/"+generatorName+".js"); 14 | const asyncHelp = `[values] NO|ALL|DETECT. 15 | NO do not wrap functions into async wrappers (default); 16 | ALL all functions will be called in a separate thread, 17 | adding last param as a callback with result argument; 18 | DETECT detect functions with 'async' and 19 | make them async as described above; 20 | `; 21 | 22 | program 23 | .version('0.2.0') 24 | .description('RuNo bridge is a command-line utility to generate C++ boilerplate to call Rust natively from Node JS, using the C ABI'); 25 | 26 | 27 | program 28 | .command('generate ') 29 | .description('generate C++ addon from Rust or JSON input') 30 | .option('--async [value]', asyncHelp) 31 | .action(function(input, output, options) { 32 | var extname = path.extname(input); 33 | let settings:IParserSettings = { 34 | verbosity: 1 35 | } 36 | if (options.async && typeof options.async == 'string') { 37 | let async: number = ASYNC[options.async.toUpperCase() as string]; 38 | if (typeof async == 'number') settings.async = async; 39 | } 40 | var p; 41 | new Promise(function(resolve, reject){ 42 | fs.stat(input, function(err, stats){ 43 | if (err || !stats.isFile()) { 44 | reject("File does not exist or not readible"); 45 | } else { 46 | resolve() 47 | } 48 | }) 49 | }).then(function(){ 50 | return new Promise(function(resolve, reject){ 51 | switch (extname) { 52 | case ".json": 53 | resolve(JSON.parse(fs.readFileSync(input, 'utf8'))); 54 | break; 55 | case ".rs": 56 | (new Parser(fs.createReadStream(input), path.basename(input,extname), settings)).parse().then(resolve); 57 | break; 58 | default: 59 | reject("Unexpected input file extension, need .json or .rs"); 60 | } 61 | }) 62 | }).then(function(inputVal){ 63 | var result = (new generator[generatorName](inputVal)).generate(); 64 | fs.writeFileSync(output,result,'utf8'); 65 | console.log("wrote content to "+output); 66 | }).catch(function(reason){ 67 | console.error(reason); 68 | }); 69 | }); 70 | 71 | program 72 | .command('parse ') 73 | .description('parse Rust file and output JSON') 74 | .option('--async [value]', asyncHelp) 75 | .action(function(input, options) { 76 | //options.async 77 | let settings:IParserSettings = { 78 | verbosity: 1 79 | } 80 | if (options.async && typeof options.async == 'string') { 81 | let async: number = ASYNC[options.async.toUpperCase() as string]; 82 | if (typeof async == 'number') settings.async = async; 83 | } 84 | var extname = path.extname(input); 85 | var p; 86 | new Promise(function(resolve, reject){ 87 | fs.stat(input, function(err, stats){ 88 | if (err || !stats.isFile()) { 89 | reject("File does not exist or not readible"); 90 | } else { 91 | resolve() 92 | } 93 | }) 94 | }).then(function(){ 95 | return new Promise(function(resolve, reject){ 96 | switch (extname) { 97 | case ".rs": 98 | (new Parser(fs.createReadStream(input), path.basename(input,extname), settings)).parse().then(resolve); 99 | break; 100 | default: 101 | reject("Unexpected input file extension, need .json or .rs"); 102 | } 103 | }) 104 | }).then(function(inputVal){ 105 | console.log(JSON.stringify(inputVal,null,' ')); 106 | }).catch(function(reason){ 107 | console.error(reason); 108 | }); 109 | }); 110 | 111 | program 112 | .command('*') 113 | .action(function(){ 114 | program.outputHelp(); 115 | }); 116 | 117 | if (!process.argv.slice(2).length) { 118 | program.outputHelp(); 119 | } 120 | 121 | program.parse(process.argv); 122 | -------------------------------------------------------------------------------- /test/generator_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require("fs"); 3 | var path = require("path"); 4 | 5 | var generatorTemplate = 'DefaultNan'; 6 | var generatorName = generatorTemplate+'Generator'; 7 | 8 | var generator = require("../dist/generators/"+generatorTemplate+"/"+generatorName+".js"); 9 | 10 | describe('Generator', function() { 11 | var input = JSON.parse(fs.readFileSync("test/resources/input.json", 'utf8')); 12 | 13 | it('should not throw when instatinated', function () { 14 | assert.doesNotThrow(function(){ 15 | new generator[generatorName](input); 16 | }, Error); 17 | }); 18 | 19 | it('should not throw when generating', function () { 20 | assert.doesNotThrow(function(){ 21 | (new generator[generatorName](input)).generate(); 22 | }, Error); 23 | }); 24 | 25 | it('generate() should return some C++ code', function () { 26 | var result = (new generator[generatorName](input)).generate(); 27 | assert(result, "returns non empty value"); 28 | assert(typeof result === 'string', "returns a string value"); 29 | assert(result.length>100, "returns not too short string"); 30 | assert(result.length>100, "returns not too short string"); 31 | assert(result.indexOf('${extern_c_functions}')<0,"result should not contain extern_c_functions template"); 32 | assert(result.indexOf('${inits}')<0,"result should not contain nan_inits template"); 33 | assert(result.indexOf('${methods}')<0,"result should not contain nan_inits template"); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/integration/test.js: -------------------------------------------------------------------------------- 1 | os = require('os'); 2 | addon = require('../resources/build/Release/addon'); 3 | var assert = require('assert'); 4 | 5 | describe('Integration', function() { 6 | it('should run int_in_int_out 2', function () { 7 | assert.equal(addon.int_in_int_out(2),4); 8 | }); 9 | 10 | it('should run is_greather_than_42 50', function () { 11 | assert.equal(addon.is_greather_than_42(50), true); 12 | }); 13 | 14 | it('should run is_greather_than_42 5', function () { 15 | assert.equal(addon.is_greather_than_42(5), false); 16 | }); 17 | 18 | it('should run get_42 true', function () { 19 | assert.equal(addon.get_42(true), 42); 20 | }); 21 | 22 | it('should run get_42 false', function () { 23 | assert.equal(addon.get_42(false), 0); 24 | }); 25 | 26 | it('should run rs_experimental_string', function () { 27 | assert.equal( 28 | addon.rs_experimental_string("Looooong string from JS dfsdfsdf sd123456789 JSEOM"), 29 | "Looooong string from JS dfsdfsdf sd123456789 JSEOM+ append from Rust" 30 | ); 31 | }); 32 | 33 | it('should run double_multiply_plus2 3.14, 2', function () { 34 | assert.equal(addon.double_multiply_plus2(3.14,2), 3.14*2+2, "double_multiply_plus2"); 35 | }); 36 | 37 | it('should run float_multiply_plus2 3.14, 2', function () { 38 | assert.equal(Math.round(addon.float_multiply_plus2(3.14,2)), Math.round(3.14*2+2), "approx equal"); 39 | assert.notEqual(addon.float_multiply_plus2(3.14,2), 3.14*2+2, "not exact equal with double calculation"); 40 | }); 41 | 42 | it('async should work', function (next) { 43 | addon.double_multiply_plus2_async(3.14,2,function(result){ 44 | assert.equal(result, 3.14*2+2, "async double_multiply_plus2"); 45 | next(); 46 | }); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/parser_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require("fs"); 3 | var path = require("path"); 4 | 5 | var parser = require("../dist/Parser.js"); 6 | 7 | var inputs = { 8 | 'int': 'test/resources/int_to_int.rs', 9 | 'embed': 'test/resources/src/embed.rs' 10 | }; 11 | 12 | describe('Parser', function() { 13 | 14 | var input = inputs['int']; 15 | var extname = path.extname(input); 16 | 17 | var inputEmbed = inputs['embed']; 18 | var extnameEmbed = path.extname(inputEmbed); 19 | 20 | it('should not throw when instatinated with read stream', function () { 21 | assert.doesNotThrow(function(){ 22 | new parser.Parser(fs.createReadStream(input), path.basename(input,extname), {verbosity: 0}); 23 | }, Error); 24 | }); 25 | 26 | it('int_to_int parse() should return promise like', function () { 27 | var p = new parser.Parser(fs.createReadStream(input), path.basename(input,extname), {verbosity: 0}); 28 | var promise = p.parse(); 29 | assert.ok(typeof promise.then === 'function'); 30 | }); 31 | 32 | it('int_to_int parse() promise should resolve', function () { 33 | var p = new parser.Parser(fs.createReadStream(input), path.basename(input,extname), {verbosity: 0}); 34 | return p.parse(); 35 | }); 36 | 37 | it('int_to_int parse() result should contain data', function () { 38 | var p = new parser.Parser(fs.createReadStream(input), path.basename(input,extname), {verbosity: 0}); 39 | return p.parse().then(function(result){ 40 | assert.equal(result.module_name, "int_to_int", "result should contain module name"); 41 | assert.deepEqual(result.functions, [{"name":"int_in_int_out","parameters":[{"name":"input","type":"c_int"}],"return":"c_int", "async": false}], "result should contain int_to_int func"); 42 | }); 43 | }); 44 | 45 | it('embed parse() result should contain data', function () { 46 | var p = new parser.Parser(fs.createReadStream(inputEmbed), path.basename(inputEmbed,extnameEmbed), {verbosity: 0}); 47 | return p.parse().then(function(result){ 48 | assert.equal(result.module_name, "embed", "result should contain module name"); 49 | assert.deepEqual(result.functions, [ 50 | {"name":"int_in_int_out","parameters":[{"name":"input","type":"c_int"}],"return":"c_int", "async": false}, 51 | {"name":"is_greather_than_42","parameters":[{"name":"input","type":"c_int"}],"return":"bool", "async": false}, 52 | {"name":"get_42","parameters":[{"name":"input","type":"bool"}],"return":"c_int", "async": false}, 53 | {"name": "rs_experimental_string", 54 | "parameters": [ 55 | { 56 | "name": "s_raw", 57 | "type": "*c_char" 58 | } 59 | ], 60 | "return": "*c_char", 61 | "async": false 62 | }, 63 | { 64 | "name":"float_multiply_plus2", 65 | "parameters":[{"name":"in1","type":"c_float"},{"name":"in2","type":"c_float"}], 66 | "return":"c_float", 67 | "async": false 68 | }, 69 | { 70 | "name":"double_multiply_plus2", 71 | "parameters":[{"name":"in1","type":"c_double"},{"name":"in2","type":"c_double"}], 72 | "return":"c_double", 73 | "async": false 74 | }, 75 | { 76 | "name":"double_multiply_plus2_async", 77 | "parameters":[{"name":"in1","type":"c_double"},{"name":"in2","type":"c_double"}], 78 | "return":"c_double", 79 | "async": false //Async should be disabled by default! 80 | } 81 | ]); 82 | }); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /test/resources/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embed" 3 | version = "0.0.2" 4 | authors = ["Andrew Kondratev "] 5 | 6 | [dependencies] 7 | libc = "0.2.6" 8 | 9 | [lib] 10 | name = "embed" 11 | crate-type = ["dylib"] 12 | -------------------------------------------------------------------------------- /test/resources/README.md: -------------------------------------------------------------------------------- 1 | ##Integration tests for RuNO bridge 2 | 3 | Does full C++ code generation, addon compilation and test from NodeJS 4 | -------------------------------------------------------------------------------- /test/resources/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [{ 3 | "target_name": "addon", 4 | "sources": ["src/addon.cc" ], 5 | 'include_dirs': [ 6 | '.', 7 | ], 8 | "include_dirs" : [ 9 | " c_int{ 7 | input*2 8 | } 9 | -------------------------------------------------------------------------------- /test/resources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runo-bridge-integration-test", 3 | "version": "0.0.1", 4 | "description": "Integration test", 5 | "scripts": { 6 | "preinstall": "node -v" 7 | }, 8 | "dependencies": { 9 | "nan": "^2.2.0" 10 | }, 11 | "license": "ISC", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/andruhon/runo-bridge" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/resources/src/embed.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | use libc::{c_char,c_int,c_float,c_double}; 4 | use std::ffi::{CStr,CString}; 5 | use std::mem; 6 | 7 | 8 | #[no_mangle] 9 | pub extern "C" fn int_in_int_out(input: c_int) -> c_int{ 10 | input*2 11 | } 12 | 13 | #[no_mangle] 14 | pub extern "C" fn is_greather_than_42(input: c_int) -> bool{ 15 | input>42 16 | } 17 | 18 | #[no_mangle] 19 | pub extern "C" fn get_42(input: bool) -> c_int{ 20 | if input { 21 | 42 22 | } else { 23 | 0 24 | } 25 | } 26 | 27 | #[no_mangle] 28 | pub extern "C" fn rs_experimental_string(s_raw: *const c_char) -> *mut c_char { 29 | // take string from the input C string 30 | if s_raw.is_null() { panic!(); } 31 | 32 | let c_str: &CStr = unsafe { CStr::from_ptr(s_raw) }; 33 | let buf: &[u8] = c_str.to_bytes(); 34 | let str_slice: &str = std::str::from_utf8(buf).unwrap(); 35 | let str_buf: String = str_slice.to_owned(); 36 | 37 | //produce a new string 38 | let result = String::from(str_buf + "+ append from Rust"); 39 | 40 | //create C string for output 41 | let c_string = CString::new(result).unwrap(); 42 | let ret: *mut c_char = unsafe {mem::transmute(c_string.as_ptr())}; 43 | mem::forget(c_string); // To prevent deallocation by Rust 44 | ret 45 | } 46 | 47 | #[no_mangle] 48 | pub extern "C" fn float_multiply_plus2(in1: c_float, in2: c_float) -> c_float{ 49 | in1*in2+(2 as c_float) 50 | } 51 | 52 | #[no_mangle] 53 | pub extern "C" fn double_multiply_plus2(in1: c_double, in2: c_double) -> c_double{ 54 | in1*in2+(2 as c_double) 55 | } 56 | 57 | #[no_mangle] 58 | pub extern "C" fn double_multiply_plus2_async(in1: c_double, in2: c_double) -> c_double{ 59 | std::thread::sleep(std::time::Duration::from_millis(50)); 60 | return in1*in2+(2 as c_double); 61 | } 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "experimentalDecorators": true, 5 | "module": "commonjs", 6 | "outDir": "dist" 7 | }, 8 | "exclude": ["node_modules"], 9 | "buildOnSave": false, 10 | "compileOnSave": false 11 | } 12 | -------------------------------------------------------------------------------- /typings/tsd.d.ts: -------------------------------------------------------------------------------- 1 | // Autogenerated, do not edit. All changes will be lost. 2 | 3 | /// 4 | /// 5 | /// 6 | /// 7 | --------------------------------------------------------------------------------