Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-13134] update documentation with wasm bindgen recommendations #455

Merged
1 change: 1 addition & 0 deletions custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ signtool
signup
sqlcmd
struct
structs
subfolders
subprocessor
toolset
Expand Down
11 changes: 11 additions & 0 deletions docs/getting-started/sdk/password-manager/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Password Manager

The Password Manager SDK is designed for internal use within Bitwarden and supports key
functionality for managing encrypted data, vault access, and user authentication. Written in Rust,
the SDK is versatile and provides bindings for a variety of platforms, including mobile clients
(Kotlin and Swift) and web clients (JavaScript/TypeScript).

This section will provide guidance on developing with the SDK in a way that ensures compatibility
across both mobile and web platforms. It will cover best practices for structuring code, addressing
platform-specific challenges, and ensuring that your implementation works seamlessly across
Bitwardenโ€™s mobile and web applications.
7 changes: 7 additions & 0 deletions docs/getting-started/sdk/password-manager/web/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Web

The Password Manager SDK supports web-based clients, allowing developers to integrate core password
management functionality into JavaScript/TypeScript environments. The SDK is packaged as an NPM
module under `@bitwarden/sdk-internal` and uses WebAssembly (`wasm`) to enable interaction with Rust
code. This section will provide guidance and best practices for working with the SDK in a browser
environment.
269 changes: 269 additions & 0 deletions docs/getting-started/sdk/password-manager/web/interoperability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# Interoperability

When working with the SDK, one of the key challenges is managing Rust/JavaScript bindings to enable
smooth interactions. In our project, we use both `wasm-bindgen` and `tsify` to bridge this gap and
make it easier for JavaScript to work with Rust code. While both tools facilitate communication
between Rust and JavaScript, they serve distinct purposes, and it's important to understand when to
use each.

## `wasm-bindgen` vs. `tsify`

:::tip

**In short:** Use `tsify` unless the web-side needs to call functions or interact with Rust objects
directly, in which case, use `wasm-bindgen`.

:::

At first glance, `wasm-bindgen` and `tsify` may appear to do similar things. Both generate
TypeScript definitions, and both allow JavaScript to work with Rust data. However, while they share
a common goal, they handle Rust/JavaScript interop in different ways and are suitable for different
use cases.

`wasm-bindgen` focuses on creating WebAssembly bindings that allow JavaScript to directly call Rust
functions and manipulate Rust objects. This works well for exposing APIs and interacting with simple
Rust types like `u32`, `f64`, and `String`. However, `wasm-bindgen` has limitations when it comes to
handling more complex Rust types such as enums with values or deeply nested structures, as these
need additional work to be represented correctly in JavaScript.

On the other hand, `tsify` leverages Rust's `serde` ecosystem to expose Rust data models to
JavaScript and automatically provide TypeScript bindings. It accomplishes this by serializing Rust
data structures into JavaScript objects, allowing them to be treated as native, typed TypesScript
objects. This differs from `wasm-bindgen`, which allows JavaScript to interact directly with
Rust-owned structures in memory. `tsify` is generally better for handling complex data types,
especially when working with Rust crates that already support `serde`.

## Choosing Between `wasm-bindgen` and `tsify`

Here's a quick overview of when to use each tool:

| Tool | Purpose | Strengths | Typical Use Case |
| -------------- | --------------------------------------- | ------------------------------ | ----------------------------------------------------------------------- |
| `wasm-bindgen` | Expose Rust functions and objects to JS | Direct Rust function calls | When JavaScript needs to call Rust functions or manipulate Rust objects |
| `tsify` | Provide TypeScript bindings via `serde` | Handle complex data structures | For exchanging structured data between Rust and JavaScript |

Use `wasm-bindgen` when you need JavaScript to call into Rust directly or work with live Rust
objects (like a struct or a function in Rust). Otherwise, default to `tsify` for easier handling of
complex types and data exchange.

## How to use `wasm-bindgen`

### Basic functions

To create functions that can be called from JavaScript using `wasm-bindgen`, you need to apply the
`wasm_bindgen` attribute to the function and ensure the return type is something that can be
converted into a JavaScript type. For instance, simple types like `u32`, `f64`, `bool`, and `String`
can be returned directly:

```rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn do_something() -> u32 {
42
}

#[wasm_bindgen]
pub fn say_hello() -> String {
"Hello, World!".to_owned()
}
```

This will generate the following TypeScript definitions:

```typescript
/**
* @returns {number}
*/
export function do_something(): number;

/**
* @returns {string}
*/
export function say_hello(): string;
```

### Structs

If you want to return more complex types, you can annotate a Rust struct with the `wasm_bindgen`
attribute. Make sure that any fields you want to access from JavaScript are public:

```rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct User {
pub age: u32,
}
```

Which generates the following TypeScript definitions:

```typescript
export class User {
free(): void;
age: number;
}
```

For structs containing `String` or other types that are not `Copy`, use the `getter_with_clone`
attribute to ensure JavaScript can access the value without directly referencing it in WebAssembly
memory:

```rust
#[wasm_bindgen(getter_with_clone)]
pub struct User {
pub name: String,
pub age: u32,
}
```

Which generates the following TypeScript definitions:

```typescript
export class User {
free(): void;
age: number;
name: string;
}
```

This ensures that values like `String` are properly cloned before being passed to JavaScript.

### Structs with Functions

You can also return types with methods:

```rust
#[wasm_bindgen(getter_with_clone)]
pub struct User {
pub name: String,
pub age: u32,
}

#[wasm_bindgen]
impl User {
pub fn say_hello(&self) -> String {
format!("Hello, {}!", self.name)
}
}
```

Which generates the following TypeScript definitions:

```typescript
export class User {
free(): void;
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
say_hello(): string;
age: number;
name: string;
}
```

## How to use `tsify`

While `wasm-bindgen` is ideal for exposing Rust functions and objects to JavaScript, `tsify` is
better suited for exposing complex data models. The `tsify` approach generates TypeScript bindings
automatically from Rust structs and works by serializing Rust data into JavaScript-compatible
objects.
justindbaur marked this conversation as resolved.
Show resolved Hide resolved

### Basic Types

To return a struct from Rust to JavaScript using `tsify`, derive `Serialize`, `Deserialize`, and
`Tsify`:

```rust
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use {tsify_next::Tsify, wasm_bindgen::prelude::*};

#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct User {
pub name: String,
pub age: u32,
pub address: Address,
}

#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct Address {
pub street: String,
pub city: String,
}
```

Which generates TypeScript interfaces for both `User` and `Address`:

```typescript
export interface User {
name: string;
age: number;
address: Address;
}

export interface Address {
street: string;
city: string;
}

export function get_user(): User;
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
```

Note how `tsify` creates TypeScript interfaces, whereas `wasm-bindgen` creates classes that
reference Rust objects stored in WebAssembly memory.

### External Types

With `tsify`, fields can be any type that `serde` can serialize or deserialize, including external
types from other crates. For example:

```rust
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;

#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct FolderView {
pub id: Option<Uuid>,
pub name: String,
pub revision_date: DateTime<Utc>,
}
```

Which generates the following TypeScript definitions:

```typescript
export interface FolderView {
id: Uuid | undefined;
name: string;
revisionDate: DateTime<Utc>;
}
```

Note that, while `tsify` does generate TypeScript definitions, external types that don't derive
`tsify` (like `Uuid` or `DateTime<Utc>`) are missing from the definitions. To fix this, external
types need to be manually added to the TypeScript definitions found in
`bitwarden-wasm-internal/src/custom_types.rs`, for example:

```rust
#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
const TS_CUSTOM_TYPES: &'static str = r#"
export type Uuid = string;

/**
* RFC3339 compliant date-time string.
* @typeParam T - Not used in JavaScript.
*/
export type DateTime<T = unknown> = string;

/**
* UTC date-time string. Not used in JavaScript.
*/
export type Utc = unknown;
"#;

```

This ensures the TypesScript definitions match the serialized Rust structure.