Skip to content

Commit a83dd24

Browse files
committed
wip
1 parent 0ad2026 commit a83dd24

File tree

2 files changed

+304
-0
lines changed

2 files changed

+304
-0
lines changed

src/doc/trpl/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* [Hello, Cargo!](hello-cargo.md)
77
* [Learn Rust](learn-rust.md)
88
* [Guessing Game](guessing-game.md)
9+
* [Rust inside other languages](rust-inside-other-languages.md)
910
* [Effective Rust](effective-rust.md)
1011
* [The Stack and the Heap](the-stack-and-the-heap.md)
1112
* [Testing](testing.md)
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
% Rust Inside Other Languages
2+
3+
For our third project, we’re going to choose something that shows off one of
4+
Rust’s greatest strengths: a lack of a substantial runtime.
5+
6+
As organizations grow, they increasingly rely on a multitude of programming
7+
languages. Different programming languages have different strengths and
8+
weaknesses, and a polyglot stack lets you use a particular language where
9+
its strengths make sense, and use a different language where it’s weak.
10+
11+
A very common area where many programming languages are weak is in runtime
12+
performance of programs. Often, using a language that is slower, but offers
13+
greater programmer productivity is a worthwhile trade-off. To help mitigate
14+
this, they provide a way to write some of your system in C, and then call
15+
the C code as though it were written in the higher-level language. This is
16+
called a ‘foreign function interface’, often shortened to ‘FFI’.
17+
18+
Rust has support for FFI in both directions: it can call into C code easily,
19+
but crucially, it can also be called _into_ as easily as C. Combined with
20+
Rust’s lack of a garbage collector and low runtime requirements, this makes
21+
Rust a great candidate to embed inside of other languages when you need
22+
some extra oomph.
23+
24+
There is a whole [chapter devoted to FFI][ffi] and its specifics elsewhere in
25+
the book, but in this chapter, we’ll examine this particular use-case of FFI,
26+
with three examples, in Ruby, Python, and JavaScript.
27+
28+
[ffi]: ffi.html
29+
30+
# The problem
31+
32+
There are many different projects we could choose here, but we’re going to
33+
pick an example where Rust has a clear advantage over many other languages:
34+
numeric computing and threading.
35+
36+
Many languages, for the sake of consistency, place numbers on the heap, rather
37+
than on the stack. Especially in languages that focus on object-oriented
38+
programming and use garbage collection, heap allocation is the default. Sometimes
39+
optimizations can stack allocate particular numbers, but rather than relying
40+
on an optimizer to do its job, we may want to ensure that we’re always using
41+
primitive number types rather than some sort of object type.
42+
43+
Second, many languages have a ‘global interpreter lock’, which limits
44+
concurrency in many situations. This is done in the name of safety, which is
45+
a positive effect, but it limits the amount of work that can be done at the
46+
same time, which is a big negative.
47+
48+
To emphasize these two aspects, we’re going to create a little project that
49+
uses these two aspects heavily. Since the focus of the example is the embedding
50+
of Rust into the languages, rather than the problem itself, we’ll just use a
51+
toy example:
52+
53+
> Start ten threads. Inside each thread, count from one to five million. After
54+
> All ten threads are finished, print out ‘done!’.
55+
56+
I chose five million based on my particular computer. Here’s an example of this
57+
code in Ruby:
58+
59+
```ruby
60+
threads = []
61+
62+
10.times do
63+
threads << Thread.new do
64+
count = 0
65+
66+
5_000_000.times do
67+
count += 1
68+
end
69+
end
70+
end
71+
72+
threads.each {|t| t.join }
73+
puts "done!"
74+
```
75+
76+
Try running this example, and choose a number that runs for a few seconds.
77+
Depending on your computer’s hardware, you may have to increase or decrease the
78+
number.
79+
80+
On my system, running this program takes `2.156` seconds. And, if I use some
81+
sort of process monitoring tool, like `top`, I can see that it only uses one
82+
core on my machine. That’s the GIL kicking in.
83+
84+
While it’s true that this is a synthetic program, one can imagine many problems
85+
that are similar to this in the real world. For our purposes, spinning up some
86+
busy threads represents some sort of parallel, expensive computation.
87+
88+
# A Rust library
89+
90+
Let’s re-write this problem in Rust. First, let’s make a new project with
91+
Cargo:
92+
93+
```bash
94+
$ cargo new embed
95+
$ cd embed
96+
```
97+
98+
This program is fairly easy to write in Rust:
99+
100+
```rust
101+
use std::thread;
102+
103+
fn process() {
104+
let handles: Vec<_> = (0..10).map(|_| {
105+
thread::spawn(|| {
106+
let mut _x = 0;
107+
for _ in (0..5_000_001) {
108+
_x += 1
109+
}
110+
})
111+
}).collect();
112+
113+
for h in handles {
114+
h.join().ok().expect(“Could not join a thread!”);
115+
}
116+
}
117+
```
118+
119+
Some of this should look familiar from previous examples. We spin up ten
120+
threads, collecting them into a `handles` vector. Inside of each thread, we
121+
loop five million times, and add one to `_x` each time. Why the underscore?
122+
Well, if we remove it and compile:
123+
124+
```bash
125+
$ cargo build
126+
Compiling embed v0.1.0 (file:///home/steve/src/embed)
127+
src/lib.rs:3:1: 16:2 warning: function is never used: `process`, #[warn(dead_code)] on by default
128+
src/lib.rs:3 fn process() {
129+
src/lib.rs:4 let handles: Vec<_> = (0..10).map(|_| {
130+
src/lib.rs:5 thread::spawn(|| {
131+
src/lib.rs:6 let mut x = 0;
132+
src/lib.rs:7 for _ in (0..5_000_001) {
133+
src/lib.rs:8 x += 1
134+
...
135+
src/lib.rs:6:17: 6:22 warning: variable `x` is assigned to, but never used, #[warn(unused_variables)] on by default
136+
src/lib.rs:6 let mut x = 0;
137+
^~~~~
138+
```
139+
140+
That first warning is because we are building a library. If we had a test
141+
for this function, the warning would go away. But for now, it’s never
142+
called.
143+
144+
The second is related to `x` versus `_x`. Because we never actually _do_
145+
anything with `x`, we get a warning about it. In our case, that’s perfectly
146+
okay, as we’re just trying to waste CPU cycles. Prefixing `x` with the
147+
underscore removes the warning.
148+
149+
Finally, we join on each thread.
150+
151+
Comparing this code to the Ruby implementation, you’ll notice that it’s very
152+
similar looking, but has more noise. Ahh, language design tradeoffs. 😊
153+
154+
Right now, however, this is a Rust library, and it doesn’t expose anything
155+
that’s callable from C. If we tried to hook this up to another language right
156+
now, it wouldn’t work. We only need to make two small changes to fix this,
157+
though. The first is modify the beginning of our code:
158+
159+
```rust,ignore
160+
#[no_mangle]
161+
pub extern fn process() {
162+
```
163+
164+
We have to add a new attribute, `no_mangle`. When you create a Rust library, it
165+
changes the name of the function in the compiled output. The reasons that this
166+
are done are outside the scope of this tutorial, but in order for other
167+
languages to know how to call the function, we need to not do that. This
168+
attribute turns that behavior off.
169+
170+
The other change is the `pub extern`. The `pub` means that this function should
171+
be callable from outside of this module, and the `extern` says that it should
172+
be able to be called from C. That’s it! Not a whole lot of change.
173+
174+
The second thing we need to do is to change a setting in our `Cargo.toml`. Add
175+
this at the bottom:
176+
177+
```toml
178+
[lib]
179+
name = "embed"
180+
crate-type = ["dylib"]
181+
```
182+
183+
This tells Rust that we want to compile our library into a standard dynamic
184+
library. By default, Rust compiles into an ‘rlib’, a Rust-specific format.
185+
186+
Let’s build the project now:
187+
188+
```bash
189+
$ cargo build --release
190+
Compiling embed v0.1.0 (file:///home/steve/src/embed)
191+
```
192+
193+
We’ve chosen `cargo build --release`, which builds with optimizations on. We
194+
want this to be as fast as possible! You can find the output of the library in
195+
`target/release`:
196+
197+
```bash
198+
$ ls target/release/
199+
build deps examples libembed.so native
200+
```
201+
202+
That `libembed.so` is our ‘shared object’ library. We can use this file
203+
just like any shared object library written in C!
204+
205+
Now that we’ve got our Rust library built, let’s use it from our Ruby.
206+
207+
# Ruby
208+
209+
Open up a `embed.rb` file inside of our project, and do this:
210+
211+
```ruby
212+
require 'ffi'
213+
214+
module Hello
215+
extend FFI::Library
216+
ffi_lib 'target/release/libembed.so'
217+
attach_function :process, [], :void
218+
end
219+
220+
Hello.process
221+
222+
puts "done!”
223+
```
224+
225+
Before we can run this, we need to install the `ffi` gem:
226+
227+
```bash
228+
$ gem install ffi # this may need sudo
229+
Fetching: ffi-1.9.8.gem (100%)
230+
Building native extensions. This could take a while...
231+
Successfully installed ffi-1.9.8
232+
Parsing documentation for ffi-1.9.8
233+
Installing ri documentation for ffi-1.9.8
234+
Done installing documentation for ffi after 0 seconds
235+
1 gem installed
236+
```
237+
238+
And finally, we can try running it:
239+
240+
```bash
241+
$ ruby embed.rb
242+
done!
243+
$
244+
```
245+
246+
Whoah, that was fast! On my system, this took `0.086` seconds, rather than
247+
the two seconds the pure Ruby version took. Let’s break down this Ruby
248+
code:
249+
250+
```ruby
251+
require 'ffi'
252+
```
253+
254+
We first need to require the `ffi` gem. This lets us interface with our
255+
Rust library like a C library.
256+
257+
```ruby
258+
module Hello
259+
extend FFI::Library
260+
ffi_lib 'target/release/libembed.so'
261+
```
262+
263+
The `ffi` gem’s authors recommend using a module to scope the functions
264+
we’ll import from the shared library. Inside, we `extend` the necessary
265+
`FFI::Library` module, and then call `ffi_lib` to load up our shared
266+
object library. We just pass it the path that our library is stored,
267+
which as we saw before, is `target/release/libembed.so`.
268+
269+
```ruby
270+
attach_function :process, [], :void
271+
```
272+
273+
The `attach_function` method is provided by the FFI gem. It’s what
274+
connects our `process()` function in Rust to a Ruby function of the
275+
same name. Since `process()` takes no arguments, the second parameter
276+
is an empty array, and since it returns nothing, we pass `:void` as
277+
the final argument.
278+
279+
```ruby
280+
Hello.process
281+
```
282+
283+
This is the actual call into Rust. The combination of our `module`
284+
and the call to `attach_function` sets this all up. It looks like
285+
a Ruby function, but is actually Rust!
286+
287+
```ruby
288+
puts "done!"
289+
```
290+
291+
Finally, as per our project’s requirements, we print out `done!`.
292+
293+
That’s it! As we’ve seen, bridging between the two languages is really easy,
294+
and buys us a lot of performance.
295+
296+
Next, let’s try Python!
297+
298+
# Python
299+
300+
# Node.js
301+
302+
Node isn’t a language, but it’s currently the dominant implementation of
303+
server-side JavaScript.

0 commit comments

Comments
 (0)