AP

Back

Running JavaScript in Rust with Deno

Exploring Deno's JavaScript Runtime in a Proof-of-Concept Rust Application for Filtering Text with JS Expressions

Written by Austin Poor

Published: 2023-05-03 | Updated 2023-08-05

#rust #javascript #deno #cli #grep
The Deno dinosaur sitting in a field in Pangea -- with a small Rust colored crab in the foreground (Generated by DALL-E)
The Deno dinosaur sitting in a field in Pangea -- with a small Rust colored crab in the foreground (Generated by DALL-E)

When I saw that Deno has open source Rust crates for running JavaScript code in a Rust application I wanted to give it a try for myself, so I created a basic, proof-of-concept project called js-in-rs to get a feel for using Deno’s crates in a Rust program — specifically the deno_core crate.

What Does it Do?

The goal of js-in-rs is to be a CLI, written in Rust, for filtering files using JavaScript expressions. It’s like the tool grep except where grep uses regular expressions, js-in-rs uses JavaScript.

The js-in-rs CLI takes two arguments — a path to an input file that will be filtered and a JavaScript expression where the the value line will be set to the value of each line in the input file and the expression’s truthiness will determine if the line should be printed.

Here’s an example of what it might look like in practice:

js-in-rs example.txt 'line.length > 5'

If the contents of the file example.txt looked like this:

A
BBBBBB
CCC
DDDDD
EEEEEEE

The output of our command would be:

BBBBBB
EEEEEEE

Only the lines of the file with more than 5 characters would be printed.

This is a pretty simple example of a JavaScript filter but due to the versitility of JavaScript, this tool is able to represent much more complex filtering logic. In many cases filters would be very hard if not impossible to express using regular expressions. Not only is JavaScript more readable but it can also be more expressive.

Say you had the following filter conditions:

  • If the line is foo, print it
  • If the line has 10-20 characters, excluding leading or trailing spaces, print it
  • Otherwise, don’t print it

Now say we have the following input file, sample.txt:

 foo
foo
  short
    this is a long line
 this line is too long, though

We could use the following grep command (I’m using the -P flag for Perl-compatable regex expressions):

grep -P '^(foo|\s*.{10,20}?\s*)$' sample.txt

And we would correctly get the following output:

foo
    this is a long line

Even in this example, we’re running into issues of readability for the regular expression and, as the discerning reader may have noticed, this regex pattern doesn’t account for all edge cases (eg trailing spaces).

By contrast, using js-in-rs we could write the command as follows:

js-in-rs sample.txt \
  'line == "foo" || (line.trim().length >= 10 && line.trim().length <= 20)'

I would argue that this is much easier to read than the revious regex pattern but even still, we could take it a step further and split it out into multiple lines — including variable assignments and comments:

js-in-rs sample.txt "$(cat <<EOF
{
  // Is the line "foo"?
  if (line === "foo") return true;

  // Is the line's length (excluding lead-/trail-ing ws) in [10,20]
  const trimLine = line.trim();
  if (trimLine.length >= 10 && trimLine.length <= 20) {
    return true;
  }

  // Otherwise, don't print...
  return false;
}
EOF
)"

Sure enough this gives us the same result:

foo
    this is a long line

Now if you’re looking back through your bash history after a month or two, you’ll have a much easier time remembering what that command does.

What if you wanted to only show lines where at least 50% of the characters in the line are uppercase? I have no idea how to do that using regular expressions. Here’s what the JavaScript filter might look like using js-in-rs:

line.length > 0 && (
  Array.from(line)
    .map(c => c === c.toUpperCase())
    .reduce((a, b) => a + b) 
  / line.length) > 0.5

Thanks to JavaScript and the Deno runtime, js-in-rs is able to be more expressive and more versitile than grep and yet simpler than writing out a full script in JavaScript and running it yourself with Node or Deno.

Show Me the Rust!

Using the Deno repository’s examples as reference I was able to get a simple example up and running without much of an issue.

After parsing the command line arguments and reading in the source file to be filtered, the program creates a single instance of the deno_core::JsRuntime that will then be reused throughout the application.

let mut runtime = JsRuntime::new(
  RuntimeOptions::default(),
);

It then iterates through the source file, line-by-line, formatting the filter into a JavaScript expression that defines an anonymous function and calls it using the source file’s line as the argument (see the above section’s explanation).

That expression is then evaluated using the JS runtime and the result is captured.

let result = runtime.execute_script(
  "matcher.js",
  js_matcher.into(),
);

The result returned can then be deserialized as a serde_json::Value enum which is expected to be a boolean value.

let scope = &mut runtime.handle_scope();
let local = v8::Local::new(scope, global);
let deserialized_value = serde_v8::from_v8::<serde_json::Value>(scope, local);

Assuming a boolean type is returned, its value will determine if the line should be printed.

match value {
  serde_json::Value::Bool(b) => {
    if b {
      println!("{}", line);
    }
  },
  _ => return Err(Error::msg(format!(
    "JS matcher must return a boolean value!",
  ))),
}

The deno_core crate does a good job of passing along error messages which can come in quite handy. For example, say you accidentially add a semicolon in the middle of your filter expression:

js-in-rs src/main.rs \
  'line.trim().length > 20 &;& line.trim().length < 50'

You would get a handy error message like the following:

Error: Eval error: Uncaught SyntaxError: Unexpected token ';'
    at matcher.js:1:39

Admitidly the line numbers may not match up completely, given the fact that the Rust application inserts the filter into a larger expression and error references the file matcher.js which isn’t real, but it is a good enough starting point to debug the issue.

So…can I "rm /usr/bin/grep"?

By now you’re probably all-in on js-in-rs and ready to delete grep entirely but before you do, remember that this is just a proof of concept. It’s very light on features, light on testing, and there’s room for improvement on the performance.

Even if this project were to continue on to add features, add tests, and boost the performance, you may be better off using a tool written entirely in Rust or entirely in JavaScript.

Rathar than embedding a JavaScript runtime in Rust, the whole tool could be written in JavaScript and compiled into a self-contained executable using Deno. Though, with that said, the compiled version of js-in-rs ends up being about half the size of the pure-JS Deno-compiled version — the release build of js-in-rs is 54 MB, grep is 179 KB, and a basic hello world JS application compiled using Deno is 103 MB.

Takeaways, Recommendations & Conclusions

Working with Deno in rust was a lot of fun. The documentation was a bit sparce but what they do have, combined with the examples they provide, was enough to get me up and running to create this small PoC that I’m calling js-in-rs.

While I don’t expect js-in-rs to unseat grep as the go-to command line tool for filtering text — and don’t even really plan to continue developing it — the experience was more than enough to pique my interest and get me thinking about all kinds of other possible applications for running JavaScript in Rust using Dino.

Rust is fast and safe but a bit slower to write and with a bit steaper of a learning curve while JavaScript is well-known language that’s fast to write but that generally runs more slowly — the two languages can fit together synergistically.

Here are a few possible applications that could benefit from running JavaScript in Rust:

  • Web-Servers/APIs with the power and performance of Rust that can be customized using JavaScript
  • Data pipelines (eg Apache Airflow) where the orchestration is handled by Rust but the high-level business logic is defined using JavaScript
  • User Defined Functions (UDFs) for databases where Rust can run JS functions in a safe sandbox

The Deno runtime also has an interesting approach to permissions where it allows users to turn on/off runtime features like network access, file system access, FFI access, environment variable access, etc. to help prevent nefarious code from accessing resources it shouldn’t. While I didn’t get a chance to explore it in this application, it’s on my list to try in the future.

If you found this interesting I highly recommend trying it out yourself. Play around with Deno’s Rust crates, create your own applications that integrate JavaScript, and if you can, document it to help build the collective knowlege base!

References


Thank you so much for reading! If you have any thoughts, questions, or comments, I'd love to hear them. You can find me on Twitter, Mastodon, or LinkedIn.

If you liked this post, you might also like: