Alex Merced I am a developer, educator, and founder of devNursery.com.

Rust, SolidJS, and Tauri: Create a cross-platform desktop app

4 min read 1123

Rust Solid Tauri Build App

On December 15, 2022 GitHub will calling it a day for the ATOM IDE, which holds a special place in software history as the first real Electron application.

Electron allowed developers to use standard web technologies that they were already familiar with to build cross-platform desktop applications for Windows, Mac, and Linux. This is all great for applications like Atom, VSCode, and Postman, but it is not always ideal. For example, one common criticism of Electron apps is that they often use up a lot of memory relative to what a lower-level language, like C, C++, Rust, or Go would use.

In this article, we’ll explore Tauri, a new framework for building binaries for most major desktop platforms. Let’s get started! To follow along with our simple example, you can access the full code here:

What is Tauri?

Tauri is a new framework that offers what people like most about Electron but fixes many of the security and performance concerns. Tauri offers the ability to design your UI using web technologies like HTML, CSS, and JavaScript but allows you to use lower-level languages to write the application and backend logic.

At the time of writing, Tauri primarily supports Rust as the backend language, but its API can be implemented across multiple languages. Over time, we can expect C, C++, Go, Ruby, Python, and even JavaScript backend bindings as users decide to implement them.

Summary of Tauri’s architecture

Like Electron, Tauri uses a multi-process approach. The Core process is essentially the backend of the application with all the data models and application logic written in Rust. The WebView process is the application UI that is built in the JavaScript framework of your choice. You can even use non-JavaScript options like Rust using Dominator or ClojureScript.

The two processes communicate either through events or commands, which are RPC calls. Either process can emit events that the other process can listen for to trigger logic in one process when the other process does a particular action.

The WebView process can issue commands to trigger pre-defined functions in the core process. These are essential RPC Calls.

Taking Tauri for a test drive

To get everything installed, I recommend following this guide from the official docs. You’ll need Node.js and Rust installed. Once everything is set up, you can create an app by running the following command:

npx create-tauri-app

You’ll be given several options of what framework or language to use for the WebView. For this tutorial, select SolidJS and then the ts-sass Solid template.

cd into the new folder and run npm install. Then, run npm run tauri dev, which turns on the Core and WebView processes so you can preview it at localhost:3000. The page essentially just displays a number that is counting each second.

Pay attention to the terminal output because there may be libraries and environmental variables you need to install to get the app to build successfully. I’m working from a POP OS, and I had to install several libraries before it had all the dependencies.

The folder structure

Our app, which I’ve called /Your-Tauri-App, will use the following folder structure:

/Your-Tauri-App
|
|-- /node_modules //All the NPM Libraries
|
|-- /src // Your frontend UI Code for the WebView Process
|
|-- /src-tauri // Your backend code for the Core process in Rust
|
|-- .gitignore // files to be ignored by git
|-- index.html // just the HTML file the WebView mounts to
|-- package-lock.json // lockfile for npm
|-- package.json // config file for npm
|-- pnpm-lock.yaml // lock file for pnpm
|-- readme.md // template readme
|-- tsconfig.json // Typescript Configurations
|-- vite.config.js // Vite configurations

Creating a basic command

You can think of commands like writing your routes in the backend of a standard web application. However, instead of using fetch to make a request to a route, we’ll use invoke to call one of these commands from our frontend.

Let’s create a very basic command in /src-tauri/src/main.rs:

#![cfg_attr(
  all(not(debug_assertions), target_os = "windows"),
  windows_subsystem = "windows"
)]


// Our Tauri Command
#[tauri::command]
fn return_string(word: String) -> String{
    return word
}

fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![return_string])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

The #[tauri::command] macro turns our function into a command, and we register our command with the app with the invoke_handler method in the Tauri builder.

Then, using the invoke method, we call this command from Solid. Let’s update our App.js file:

import { invoke } from '@tauri-apps/api/tauri';
import { Component, createSignal } from 'solid-js';
import './App.scss';
import Counter from './Counter';

const App: Component = () => {
  const [counter, setCounter] = createSignal(0);
  setInterval(setCounter, 1000, (c: number) => c + 1);

  async function handleClick(event){

    // Invoke the command from our backend using the invoke function
    const result = await invoke("return_string", {
      word: "This is the argument"
    })

    // print the result to an alert
    alert(result)
  }

  return (
    <>
      <div>
        <h1 class="header">{counter()}</h1>
        <button onClick={handleClick}>Click Me</button>
      </div>
      <Counter />
    </>
  );
};

export default App;

In the code above, we import the invoke method import { invoke } from '@tauri-apps/api/tauri';. We call invoke and pass it two arguments, a string with the name of the command to invoke and an object with any argument by name.

Now, when we click on the new Click Me button, the text we sent will be sent back, confirming that we successfully invoked the method from our backend. That’s it! You can create commands using Rust as well as any Rust libraries to handle data management needs for your application.

Building the application

At the time of writing, Tauri doesn’t support cross-compilation, so it’ll compile to whatever operating system you are currently on, in my case, Linux.

To enable this, all you have to do is run npm run tauri build. Just make sure to update the identifier property in the tauri.config.json to whatever you want.

For me, after the build completed, there were AppImage and Deb files available in src-tauri/target/bundle.



Conclusion

Tauri opens up desktop apps using web technologies and your favorite frontend framework to better performance and smaller bundles. Tauri will certainly be tempting many Electron developers into making the switch for apps that need improved performance.

Although we just scratched the surface of what Tauri is capable of, I recommend reading up on the following cool features:

Happy coding!

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Alex Merced I am a developer, educator, and founder of devNursery.com.

Leave a Reply