├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── README.md ├── app ├── index.js └── templates │ ├── Cargo.toml │ ├── README.md │ ├── editorconfig │ ├── gitattributes │ ├── gitignore │ ├── js │ ├── lib │ │ └── index.js │ └── tests │ │ └── index.js │ ├── jshintrc │ ├── package.json │ ├── src │ └── lib.rs │ └── tests │ └── lib.rs ├── package.json └── test └── test-app.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "bitwise": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "undef": true, 14 | "unused": true, 15 | "-W106": true 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust FFI Generator 2 | 3 | The Rust FFI generator is a yeoman generator for bootstraping [Rust](http://www.rust-lang.org/) libraries that expose Foreign Function Interface bindings for other languages (currently only NPM is supported). 4 | 5 | This project was inspired by [Zbigniew Siciarz's blog post on calling Rust from other languages](https://siciarz.net/24-days-of-rust-calling-rust-from-other-languages/). 6 | 7 | In order to use this properly, you will obviously need Rust installed. The current convention for this is to use the lastest Nightly Release. Instructions for this can be found in [the Rust Book](http://doc.rust-lang.org/book/installing-rust.html). 8 | 9 | ## Install 10 | Install Yeoman (best when installed globally). 11 | ```bash 12 | npm install -g yo 13 | ``` 14 | 15 | Install this generator. 16 | ```bash 17 | npm install -g generator-rust-ffi 18 | ``` 19 | 20 | Create a folder for your project to live. 21 | ```bash 22 | mkdir a-rust-lib && cd a-rust-lib 23 | ``` 24 | 25 | Finally, initiate the generator. 26 | ```bash 27 | yo rust-ffi 28 | ``` 29 | 30 | ## Binding-Specific Install 31 | You will also need to install tools required by the bindings you create (obviously). 32 | 33 | ### JavaScript (nodejs/io.js) 34 | You need to install a modern version of nodejs/io.js (v0.10+). 35 | 36 | ## License 37 | MIT 38 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /* jshint -W106 */ 2 | 'use strict'; 3 | var yeoman = require('yeoman-generator'); 4 | var chalk = require('chalk'); 5 | var yosay = require('yosay'); 6 | var slug = require('slug'); 7 | var GitHubApi = require('github'); 8 | 9 | var github = new GitHubApi({ version: '3.0.0' }); 10 | 11 | var githubUserInfo = function (name, cb, log) { 12 | github.user.getFrom({ 13 | user: name 14 | }, function (err, res) { 15 | if (err) { 16 | log.error('Cannot fetch your github profile. Make sure you\'ve typed it correctly.'); 17 | res = { 18 | name: '', 19 | email: '', 20 | html_url: '' 21 | }; 22 | } 23 | cb(JSON.parse(JSON.stringify(res))); 24 | }); 25 | }; 26 | 27 | var extractGeneratorName = function (appname) { 28 | var slugged = slug(appname); 29 | var match = slugged.match(/^generator-(.+)/); 30 | 31 | if (match && match.length === 2) { 32 | return match[1].toLowerCase(); 33 | } 34 | return slugged; 35 | }; 36 | 37 | module.exports = yeoman.generators.Base.extend({ 38 | initializing: function () { 39 | this.pkg = require('../package.json'); 40 | }, 41 | 42 | prompting: { 43 | greet: function() { 44 | this.log(yosay('Create your own ' + chalk.red('Rust') + ' library with FFI bindings!')); 45 | }, 46 | 47 | askForBindings: function() { 48 | var done = this.async(); 49 | 50 | var prompts = [ 51 | { type: 'checkbox', 52 | name: 'bindings', 53 | message: 'What kind of bindings would you like to create?', 54 | choices: [ 55 | { name: 'NPM Module', 56 | value: { 57 | name: 'JavaScript', 58 | slugname: 'js', 59 | description: 'An NPM Module', 60 | build: 'To build the Rust library, along with the installing node dependencies, run:\n```$ npm i```', 61 | test: 'To run Mocha tests, run:\n```$ npm test```' 62 | }, 63 | checked: true } 64 | ] } 65 | ]; 66 | 67 | this.prompt(prompts, function(props) { 68 | var bindings = {}; 69 | props.bindings.forEach(function(binding) { 70 | bindings[binding.slugname] = binding; 71 | }); 72 | this.bindings = bindings; 73 | 74 | done(); 75 | }.bind(this)); 76 | }, 77 | 78 | askForGitHubUser: function () { 79 | var done = this.async(); 80 | 81 | var prompts = [{ 82 | name: 'githubUser', 83 | message: 'Would you mind telling me your username on GitHub?', 84 | default: 'someuser' 85 | }]; 86 | 87 | this.prompt(prompts, function (props) { 88 | this.githubUser = props.githubUser; 89 | 90 | done(); 91 | }.bind(this)); 92 | }, 93 | 94 | askForProjectProps: function () { 95 | var done = this.async(); 96 | 97 | var prompts = [ 98 | { name: 'name', 99 | message: 'Module name', 100 | default: extractGeneratorName(this.appname) }, 101 | { name: 'description', 102 | message: 'Description', 103 | default: 'A Rust library with bindings!' }, 104 | { name: 'keywords', 105 | message: 'Key your keywords (comma to split)', 106 | default: '' }, 107 | { name: 'license', 108 | message: 'License', 109 | default: 'MIT' } 110 | ]; 111 | this.prompt(prompts, function (props) { 112 | this.name = props.name; 113 | this.slugname = slug(this.name); 114 | this.description = props.description; 115 | this.keywords = props.keywords; 116 | this.license = props.license; 117 | 118 | done(); 119 | }.bind(this)); 120 | } 121 | }, 122 | 123 | configuring: { 124 | userInfo: function () { 125 | var done = this.async(); 126 | 127 | githubUserInfo(this.githubUser, function (res) { 128 | /*jshint camelcase:false */ 129 | this.author = res.name; 130 | this.email = res.email; 131 | this.githubUrl = res.html_url; 132 | this.repoUrl = this.githubUrl + '/' + this.slugname; 133 | done(); 134 | }.bind(this), this.log); 135 | }, 136 | 137 | keywords: function() { 138 | this.keywords = this.keywords.split(',').map(function(el) { 139 | return el.trim(); 140 | }).filter(function(el) { 141 | return !!el; 142 | }); 143 | } 144 | }, 145 | 146 | writing: { 147 | jsFiles: function () { 148 | if (!this.bindings.js) { return; } 149 | this.template('package.json'); 150 | this.copy('jshintrc', '.jshintrc'); 151 | this.template('js/lib/index.js'); 152 | this.template('js/tests/index.js'); 153 | }, 154 | 155 | rustFiles: function() { 156 | this.template('Cargo.toml'); 157 | this.copy('src/lib.rs'); 158 | this.template('tests/lib.rs'); 159 | }, 160 | 161 | projectFiles: function () { 162 | this.copy('editorconfig', '.editorconfig'); 163 | this.copy('gitignore', '.gitignore'); 164 | this.copy('gitattributes', '.gitattributes'); 165 | this.template('README.md'); 166 | } 167 | }, 168 | 169 | install: function () { 170 | if (this.bindings.js) { 171 | this.installDependencies({ 172 | skipInstall: this.options['skip-install'], 173 | bower: false 174 | }); 175 | } 176 | } 177 | }); 178 | -------------------------------------------------------------------------------- /app/templates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "<%= slugname %>" 4 | version = "0.0.1" 5 | authors = ["<%= author %> <<%= email %>>"] 6 | license = "<%= license %>" 7 | readme = "README.md" 8 | repository = "<%= repoUrl %>" 9 | description = "<%= description %>" 10 | 11 | [lib] 12 | name = "<%= slugname %>" 13 | crate-type = ["rlib", "dylib"] 14 | -------------------------------------------------------------------------------- /app/templates/README.md: -------------------------------------------------------------------------------- 1 | # <%= name %> 2 | <%= description %> 3 | 4 | **Table of Contents** 5 | - [About](#about) 6 | <% for (var key in bindings) { var binding = bindings[key]; %> 7 | - [<%= binding.name %>](#<%= binding.slugname %>) 8 | 9 | <% } %> 10 | ## About 11 | This is a Rust crate that also contains bootstrapping for creating bindings in other languages. 12 | 13 | ## Bindings 14 | 15 | <% for (var key in bindings) { var binding = bindings[key]; %> 16 | ### <%= binding.name %> 17 | <%= binding.description %> 18 | 19 | #### Build 20 | <%= binding.build %> 21 | 22 | #### Test 23 | <%= binding.test %> 24 | 25 | <% } %> 26 | ## License 27 | <%= license %> 28 | -------------------------------------------------------------------------------- /app/templates/editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.rs] 15 | indent_size = 4 16 | <% if (bindings.js) { %> 17 | 18 | [*.js] 19 | indent_size = 2 20 | <% } %> 21 | -------------------------------------------------------------------------------- /app/templates/gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /app/templates/gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Rust # 3 | ################################################################################ 4 | # Compiled files 5 | *.o 6 | *.so 7 | *.rlib 8 | *.dll 9 | 10 | # Executables 11 | *.exe 12 | 13 | # Generated by Cargo 14 | /target/ 15 | <% if (bindings.js) { %> 16 | 17 | ################################################################################ 18 | # Node # 19 | ################################################################################ 20 | # Logs 21 | logs 22 | *.log 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (http://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directory 45 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 46 | node_modules 47 | <% } %> 48 | -------------------------------------------------------------------------------- /app/templates/js/lib/index.js: -------------------------------------------------------------------------------- 1 | var FFI = require('ffi'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var ref = require('ref'); 5 | var Struct = require('ref-struct'); 6 | 7 | var libPath; 8 | fs.readdirSync(path.resolve(__dirname, '../../target/release')).forEach(function(file) { 9 | if (/lib<%= slugname %>/.test(file)) { 10 | libPath = file; 11 | } 12 | }); 13 | 14 | var HelloType = new Struct({ 15 | msg: 'string' 16 | }); 17 | var HelloTypePtr = ref.refType(HelloType); 18 | 19 | var lib = FFI.Library(path.resolve(__dirname, '../../target/release', libPath), { 20 | 'hello_rust': [ 'byte', [ 'string' ] ], 21 | 'hello_struct': [ 'void', [ HelloTypePtr ] ] 22 | }); 23 | 24 | module.exports = exports = { 25 | HelloType: HelloType, 26 | helloRust: lib.hello_rust, 27 | helloStruct: function(obj) { 28 | // Convert to C-style struct before passing 29 | var passable = new HelloType(obj); 30 | lib.hello_struct(passable.ref()); 31 | for (var key in obj) { 32 | obj[key] = passable[key] || obj[key]; 33 | } 34 | }, 35 | lib: lib 36 | }; 37 | -------------------------------------------------------------------------------- /app/templates/js/tests/index.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var lib = require('../lib'); 4 | 5 | describe('<%= name %>', function() { 6 | it('Should return 1 after calling hello_rust', function() { 7 | expect(lib.helloRust('<%= name %>')).to.be.equal(1); 8 | }); 9 | 10 | it('Should be able to get mutated input from hello_struct', function() { 11 | var obj = { msg: 'Hello Rust!' }; 12 | lib.helloStruct(obj); 13 | console.log(obj.msg); 14 | expect(obj).to.deep.equal({ msg: 'Hello JS!' }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/templates/jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "bitwise": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "undef": true, 14 | "unused": true, 15 | "-W106": true 16 | } 17 | -------------------------------------------------------------------------------- /app/templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= slugname %>", 3 | "description": "<%= description %>", 4 | "main": "js/lib/index.js", 5 | "version": "0.0.1", 6 | "author": { 7 | "name": "<%= author %>", 8 | "email": "<%= email %>", 9 | "url": "<%= githubUrl %>" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "<%= repoUrl %>" 14 | }, 15 | "license": "<%= license %>", 16 | "keywords": [ 17 | "<%= slugname %>"<% for (var i = 0; i < keywords.length; i++) { %>, 18 | "<%= keywords[i] %>"<% } %> 19 | ], 20 | "scripts": { 21 | "test": "npm i && ./node_modules/.bin/mocha -R spec js/tests/**/*.js", 22 | "install": "cargo build --release --verbose" 23 | }, 24 | "dependencies": { 25 | "ffi": "^1.2.7", 26 | "ref": "^1.0.1", 27 | "ref-struct": "^1.0.1" 28 | }, 29 | "devDependencies": { 30 | "chai": "^2.1.0", 31 | "mocha": "^2.2.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/templates/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::ffi::CString; 3 | 4 | #[no_mangle] 5 | pub extern "C" fn hello_rust(name: *const i8) -> u8 { 6 | let buf = unsafe { CStr::from_ptr(name).to_bytes() }; 7 | let str_name = String::from_utf8(buf.to_vec()).unwrap(); 8 | println!("Hello {}!", str_name); 9 | 1 10 | } 11 | 12 | #[repr(C)] 13 | pub struct hello_t { 14 | pub msg: *const i8 15 | } 16 | 17 | #[no_mangle] 18 | pub extern "C" fn hello_struct(obj: &mut hello_t) { 19 | let buf = unsafe { CStr::from_ptr(obj.msg).to_bytes() }; 20 | let str_msg = String::from_utf8(buf.to_vec()).unwrap(); 21 | println!("{}", str_msg); 22 | obj.msg = CString::new("Hello JS!").unwrap().as_ptr(); 23 | } 24 | -------------------------------------------------------------------------------- /app/templates/tests/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate <%= slugname %>; 2 | 3 | use std::ffi::CStr; 4 | use std::ffi::CString; 5 | 6 | #[test] 7 | fn hello_struct_test() { 8 | let msg = CString::new("Hello Rust!").unwrap().as_ptr(); 9 | let h = <%= slugname %>::hello_t { 10 | msg: msg 11 | }; 12 | <%= slugname %>::hello_struct(h); 13 | let buf = unsafe { CStr::from_ptr(h.msg).to_bytes() }; 14 | let str_msg = String::from_utf8(buf.to_vec()).unwrap(); 15 | assert_eq!("Hello JS!", str_msg); 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-rust-ffi", 3 | "version": "0.0.1", 4 | "description": "Yeoman generator", 5 | "license": "MIT", 6 | "main": "app/index.js", 7 | "repository": "oppenlander/generator-rust-ffi", 8 | "author": { 9 | "name": "Andrew Oppenlander", 10 | "email": "andrew.oppenlander@gmail.com", 11 | "url": "https://github.com/oppenlander" 12 | }, 13 | "engines": { 14 | "node": ">=0.10.0" 15 | }, 16 | "scripts": { 17 | "test": "mocha" 18 | }, 19 | "files": [ 20 | "app" 21 | ], 22 | "keywords": [ 23 | "yeoman-generator", 24 | "rust", 25 | "ffi" 26 | ], 27 | "dependencies": { 28 | "chalk": "^1.0.0", 29 | "github": "^0.2.3", 30 | "slug": "^0.8.0", 31 | "yeoman-generator": "^0.18.9", 32 | "yosay": "^1.0.2" 33 | }, 34 | "devDependencies": { 35 | "mocha": "*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/test-app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var assert = require('yeoman-generator').assert; 5 | var helpers = require('yeoman-generator').test; 6 | var os = require('os'); 7 | 8 | describe('rust-ffi:app', function () { 9 | before(function (done) { 10 | helpers.run(path.join(__dirname, '../app')) 11 | .inDir(path.join(os.tmpdir(), './temp-test')) 12 | .withOptions({ 'skip-install': true }) 13 | .withPrompt({ 14 | someOption: true 15 | }) 16 | .on('end', done); 17 | }); 18 | 19 | it('creates files', function () { 20 | assert.file([ 21 | 'package.json', 22 | '.editorconfig', 23 | '.jshintrc' 24 | ]); 25 | }); 26 | }); 27 | --------------------------------------------------------------------------------