Pushing Native Messaging to the limits! - C vs Rust
Tags: c, webperf, webdev
Published: 2020-06-04 14:51:12

I recently stumbled upon [Native Messaging](https://stackoverflow.com/questions/25169384/what-is-native-messaging-between-applications-and-how-does-it-work), which is basically the way for a browser extension (chrome or firefox) to communicate with a native application. A friend was implementing this feature in his rust app and I decided to help out, which also was enough of a motivation for me to finally start learning Rust.

## Native Messaging - Illustrated
![MDN](https://media.prod.mdn.mozit.cloud/attachments/2016/08/31/13833/81ec4685b1f9f8745e12d96fdb5b33a8/native-messaging.png)

*Courtesy of MDN*

## Libraries used for Native Messaging
* [For C](https://github.com/TotallyNotChase/lib-native-messaging/tree/master)
  
  *made by yours truly - shameless plug*

  No but seriously, I didn't find another library for native messaging in C on github, though there were 2 for Rust.
* [For Rust](https://github.com/neon64/chrome-native-messaging)

## The story (also performance is love)
I already had a pretty decent base built up on the low level and after reading the very well documented [MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging) and [Google](https://developer.chrome.com/extensions/nativeMessaging) docs, and with major help from [this crate's code](https://github.com/neon64/chrome-native-messaging) I had these functions-
```rust
pub fn read_input<R: Read>(mut input: R) -> io::Result<serde_json::Value> {
    let length = input.read_u32::<NativeEndian>().unwrap();
    let mut buffer = vec![0; length as usize];
    input.read_exact(&mut buffer)?;
    let json_val: serde_json::Value = serde_json::from_slice(&buffer).unwrap();
    Ok(json_val)
}

pub fn write_output<W: Write>(mut output: W, value: &serde_json::Value) -> io::Result<()> {
    let msg = serde_json::to_string(value)?;
    let len = msg.len();
    // Chrome won't accept a message larger than 1MB
    if len > 1024 * 1024 {
        panic!("Message was too large", length: {}, len)
    }
    output.write_u32::<NativeEndian>(len as u32)?;
    output.write_all(msg.as_bytes())?;
    output.flush()?;
    Ok(())
}
```
Which facilitated reading and writing the messages respectively. And it all worked like a charm!

Now I'm really obsessed with performance, probably a bit more than I should be, though I *usually* don't prefer performance over resource efficiency and safety. So I had to dig further.

I decided to write this in C and also make the Rust code just a bit better for performance and also make both codebases as fair as possible. They both had to do exactly the same thing. Nothing more, nothing less.

So I removed the `serde_json` completely and just worked with strings instead, the user can parse themselves. So instead of-
```rust
let json_val: serde_json::Value = serde_json::from_slice(&buffer).unwrap();
```
I used-
```rust
let val: String = match String::from_utf8(buffer) {
        Err(why) => panic!(why.to_string()),
        Ok(val) => val
    };
```
Same for the output too, I just removed `let msg = serde_json::to_string(value)?;` completely and changed the parameter to `&str` instead.

Not much difference syntactically but definitely a great boost to performance! The `C` code also did the same thing, here's how that looked like-

```c
char* read_input(FILE* stream)
{
    uint32_t length;
    size_t count;
    int err;
    count = fread(&length, sizeof(uint32_t), 1, stream);
    if (count != 1)
    {
        if (feof(stream))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stream)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stream);
        }
        return NULL;
    }
    char* value = malloc((length + 1) * sizeof(char));
    if (value == NULL)
    {
        fprintf(stderr, "An error occured while allocating memory for value");
        return NULL;
    }
    count = fread(value, sizeof(char), length, stream);
    if (count != length)
    {
        if (feof(stream))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stream)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stream);
        }
        free(value);
        return NULL;
    }
    value[length] = '\0';
    return value;
}

size_t write_output(const char* const value, uint32_t length, FILE* stream)
{
    size_t count;
    int err;
    if (length > (1024 * 1024)) {
        fprintf(stderr, "Message too large");
        return -1;
    }
    count = fwrite(&length, sizeof(uint32_t), 1, stream);
    if (count != 1)
    {
        if (feof(stream))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stream)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stream);
        }
        return -1;
    }
    count = fwrite(value, sizeof(char), length, stream);
    if (count != length)
    {
        if (feof(stream))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while writing file\n");
        }
        else if ((err = ferror(stream)))
        {
            fprintf(stderr, "An error occured while writing file, err code: %d\n", err);
            clearerr(stream);
        }
        return -1;
    }
    fflush(stream);
    return length + 4;
}
```

Yeah I know, a lot of error handling. But! It's time to benchmark! So how do they fare?

Hold up! Not quite yet! Let's check the profile I used for optimization on the rust side
```toml
[profile.release]
opt-level = 3
debug = false
debug-assertions = false
overflow-checks = false
lto = true
panic = 'abort'
incremental = false
rpath = false
```
Looks quite juicy for the benchmark right? I'd say so!

What about C? Well, I didn't have a `gcc` + Native Messaging setup handy so I had to use `clang`.....on windows, long story. This was kind of a disadvantage. All I could do was `-Ofast`. That's it.

Yes, I know. Not very fair on the optimization side. But I did some testing on the non optimized versions of both, and C was absolutely wiping the floor so I figured - "whatever"

Also the benchmark was all done on the javascript side and the following is the **connectionless** version. So each time a message is sent, the extension invokes the executable, waits for it to read *and* send a response. This is how it looked like-

```js
var start;

chrome.browserAction.onClicked.addListener(() => {
    console.log('Sending: ping')
    start = performance.now();
    chrome.runtime.sendNativeMessage("pingpong", {text: "ping"}, onResponse);
});

function onResponse(res) {
    let end = performance.now();
    console.log(`Received: ${res.msg}, Took: ${end - start} ms`);
}
```

Alright benchmark time, here's what rust looked like-

![Rust](https://cdn.discordapp.com/attachments/716700841196650536/717433620914241636/rustbench.png)

And here's C-

![C](https://cdn.discordapp.com/attachments/716700841196650536/717433640425881640/cbench.png)

Close, very close! But C still seems to win just a bit over. Impressive results nevertheless, how about some more optimization?

That question lead to many, many code revisions - a lot of rust programmers helped me out so much to optimize the code. So after many, many revisions. This is how the rust code looked like-
```rust
pub fn read_input() -> io::Result<Vec<u8>> {
    let mut instream = io::stdin();
    let mut length = [0; 4];
    instream.read(&mut length)?;
    let mut buffer = vec![0; u32::from_ne_bytes(length) as usize];
    instream.read_exact(&mut buffer)?;
    Ok(buffer)
}

pub fn write_output(msg: &str) -> io::Result<()> {
    let mut outstream = io::stdout();
    let len = msg.len();
    if len > 1024 * 1024 {
        panic!("Message was too large, length: {}", len)
    }
    outstream.write(&len.to_ne_bytes())?;
    outstream.write_all(msg.as_bytes())?;
    outstream.flush()?;
    Ok(())
}
```
Got rid of all dependencies and instead of converting to `String`, returned the raw bytes. Also make `stdin` and `stdout` constants as those are always the streams used for Native Messaging anyway.

The C code also changed accordingly, though not as many revisions-

```C
uint8_t* read_input()
{
    uint32_t length;
    size_t count;
    int err;
    count = fread(&length, sizeof(uint32_t), 1, stdin);
    if (count != 1)
    {
        if (feof(stdin))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdin)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdin);
        }
        return NULL;
    }
    uint8_t* value = malloc((length + 1) * sizeof(*value));
    if (value == NULL)
    {
        fprintf(stderr, "An error occured while allocating memory for value");
        return NULL;
    }
    count = fread(value, sizeof(*value), length, stdin);
    if (count != length)
    {
        if (feof(stdin))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdin)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdin);
        }
        free(value);
        return NULL;
    }
    return value;
}

size_t write_output(const uint8_t* const value, uint32_t length)
{
    size_t count;
    int err;
    if (length > (1024 * 1024)) {
        fprintf(stderr, "Message too large");
        return 0;
    }
    count = fwrite(&length, sizeof(uint32_t), 1, stdout);
    if (count != 1)
    {
        if (feof(stdout))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdout)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdout);
        }
        return 0;
    }
    count = fwrite(value, sizeof(char), length, stdout);
    if (count != length)
    {
        if (feof(stdout))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while writing file\n");
        }
        else if ((err = ferror(stdout)))
        {
            fprintf(stderr, "An error occured while writing file, err code: %d\n", err);
            clearerr(stdout);
        }
        return 0;
    }
    fflush(stdout);
    return length + 4;
}
```

And thus, it was benchmark time again! The final iteration!

Here's Rust-

![Rust](https://cdn.discordapp.com/attachments/716700841196650536/717439306544971859/rustbench.png)

And here's C-

![C](https://cdn.discordapp.com/attachments/716700841196650536/717439312647553054/cbench.png)

Well, would you look at that...not much difference from the last time. Well, I sure as hell was disappointed.

Just as I was about to call it a ~~day~~ benchmark, someone suggested I should use **connectionful** messaging, because rust executable invokations take a long time, far longer than C due to panics and a whole lot of other setup. I decided, "sure I could try that!"

So I switched my js code to this-
```js
var start;
var port = chrome.runtime.connectNative('pingpong');
port.onMessage.addListener(function(res) {
    let end = performance.now();
    console.log(`Received: ${res.msg}, took: ${(end - start) * 1000} μs`);
});
port.onDisconnect.addListener(function() {
    console.log("Disconnected");
});
chrome.browserAction.onClicked.addListener(() => {
    console.log("Sending: ping")
    start = performance.now();
    port.postMessage({ text: "ping" });
});
```

And also changed my native app to use an infinite loop - to keep listening for input

This actually brought the accuracy down to microseconds! So I had to change the timer to that instead, how astonishing!

And thus, here be the final results-

Rust-

![Rust](https://dev-to-uploads.s3.amazonaws.com/i/runxumlluflr8aa1o79m.png)

C-

![C](https://dev-to-uploads.s3.amazonaws.com/i/n5th3ad91llz1wtzjros.png)

Incredible! Now that's a feat. Averaging these 2 results, with 1 anomaly from each result discarded (1k+ microseconds for C and 600+ microseconds for Rust), we can see that C beats Rust by just 20-30 microseconds!

And thus, **C is the winner!** Though Rust put up a very admirable fight.

If you'd like to test out the code yourself, and I insist you do as my benchmarking really isn't very sophisticated, you can find the code in my gists-
* [C gist](https://gist.github.com/TotallyNotChase/8c9e79ed967ae911869aec82545b21ae)
* [Rust gist](https://gist.github.com/TotallyNotChase/c747c55d4a965954f49a7fa5c3f344e0)

This has been very fun! And a super nice start to my rust journey! I think this was a pretty practical demonstration of stdin and stdout IO speeds for Rust vs C. I hope to continue these benchmarks in the future, hopefully focusing on other practical topics.

Once again, if you're looking forward to implementing Native Messaging in your C application, please check out [libnativemsg](https://github.com/TotallyNotChase/lib-native-messaging/tree/master)!