Skip to main content

Custom JsValue Conversions

While the standard Wasm derive uses serde-based serialization, you can define custom conversion logic with the js_value attribute.

Basic JsValue Conversion

use wasm_bindgen::prelude::*;
use bomboni_wasm::Wasm;

#[derive(Wasm)]
#[wasm(js_value)]
pub struct CustomId(u64);
This generates a type that converts to/from JsValue using the default conversions.

String Conversion

For types that have a Display and FromStr implementation:
use std::fmt;
use std::str::FromStr;
use bomboni_wasm::Wasm;

#[derive(Wasm)]
#[wasm(js_value(convert_string))]
pub struct UserId(String);

impl fmt::Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl FromStr for UserId {
    type Err = String;
    
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(UserId(s.to_string()))
    }
}
Generated TypeScript:
export type UserId = string;
JavaScript usage:
const userId = "user-123"; // Just a string!
processUser(userId);

Custom Conversion Functions

Define your own conversion logic:
use wasm_bindgen::prelude::*;
use bomboni_wasm::Wasm;

#[derive(Clone, Wasm)]
#[wasm(js_value(
    into = CustomType::to_js_value,
    try_from = CustomType::from_js_value,
))]
pub struct CustomType {
    value: String,
}

impl CustomType {
    fn to_js_value(self) -> JsValue {
        // Custom serialization logic
        JsValue::from_str(&self.value)
    }
    
    fn from_js_value(js: JsValue) -> Result<Self, JsValue> {
        // Custom deserialization logic
        match js.as_string() {
            Some(value) => Ok(CustomType { value }),
            None => Err(JsValue::from_str("Expected a string")),
        }
    }
}

Proxy Types

Proxy types allow you to use an intermediate type for WASM conversions, useful when the Rust type doesn’t directly map to JavaScript.

Basic Proxy

use wasm_bindgen::prelude::*;
use bomboni_wasm::Wasm;

// The proxy type with WASM bindings
#[wasm_bindgen]
pub struct DateProxy {
    timestamp: f64,
}

// Your actual Rust type
#[derive(Wasm)]
#[wasm(proxy = DateProxy)]
pub struct DateTime {
    seconds: i64,
    nanos: u32,
}

impl From<DateTime> for DateProxy {
    fn from(dt: DateTime) -> Self {
        DateProxy {
            timestamp: dt.seconds as f64 + (dt.nanos as f64 / 1_000_000_000.0),
        }
    }
}

impl TryFrom<DateProxy> for DateTime {
    type Error = String;
    
    fn try_from(proxy: DateProxy) -> Result<Self, Self::Error> {
        let seconds = proxy.timestamp.trunc() as i64;
        let nanos = ((proxy.timestamp.fract()) * 1_000_000_000.0) as u32;
        Ok(DateTime { seconds, nanos })
    }
}

Proxy with Custom Conversions

#[derive(Wasm)]
#[wasm(proxy(
    source = DateProxy,
    into = DateTime::to_proxy,
    try_from = DateTime::from_proxy,
))]
pub struct DateTime {
    seconds: i64,
    nanos: u32,
}

impl DateTime {
    fn to_proxy(self) -> DateProxy {
        DateProxy {
            timestamp: self.seconds as f64 + (self.nanos as f64 / 1_000_000_000.0),
        }
    }
    
    fn from_proxy(proxy: DateProxy) -> Result<Self, String> {
        let seconds = proxy.timestamp.trunc() as i64;
        let nanos = ((proxy.timestamp.fract()) * 1_000_000_000.0) as u32;
        Ok(DateTime { seconds, nanos })
    }
}

Console Logging

Bomboni provides convenient macros for browser console output.

Console Log Macro

use bomboni_wasm::console_log;

#[wasm_bindgen]
pub fn process_data(items: Vec<String>) {
    console_log!("Processing {} items", items.len());
    
    for (i, item) in items.iter().enumerate() {
        console_log!("Item {}: {}", i, item);
    }
    
    console_log!("Processing complete");
}
JavaScript output:
Processing 3 items
Item 0: foo
Item 1: bar
Item 2: baz
Processing complete

Console Error Macro

use bomboni_wasm::{console_log, console_error};

#[wasm_bindgen]
pub fn risky_operation() -> Result<(), JsValue> {
    console_log!("Starting risky operation...");
    
    match perform_operation() {
        Ok(result) => {
            console_log!("Success: {:?}", result);
            Ok(())
        }
        Err(e) => {
            console_error!("Operation failed: {}", e);
            Err(JsValue::from_str(&e.to_string()))
        }
    }
}

Direct Console Functions

For cases where you don’t need formatting:
use bomboni_wasm::macros::{log, error};

#[wasm_bindgen]
pub fn simple_logging() {
    log("This is a log message");
    error("This is an error message");
}

wasm-bindgen Integration

Generated Traits

The Wasm derive generates wasm-bindgen conversion traits:
use bomboni_wasm::Wasm;

#[derive(Serialize, Deserialize, Wasm)]
#[wasm(into_wasm_abi, from_wasm_abi)]
pub struct Data {
    pub value: String,
}
This generates:
  • IntoWasmAbi - Convert Rust type to WASM ABI
  • FromWasmAbi - Convert WASM ABI to Rust type
  • OptionIntoWasmAbi - Handle Option<Data>
  • OptionFromWasmAbi - Handle Option<Data>
  • VectorIntoWasmAbi - Handle Vec<Data>
  • VectorFromWasmAbi - Handle Vec<Data>
  • RefFromWasmAbi - Handle &Data
  • LongRefFromWasmAbi - Handle long-lived references

Using in Function Signatures

With ABI traits generated, you can use your types directly:
#[wasm_bindgen]
pub fn process_item(item: Data) -> Data {
    // item is automatically converted from JavaScript
    Data {
        value: item.value.to_uppercase(),
    }
    // Return value is automatically converted to JavaScript
}

#[wasm_bindgen]
pub fn process_optional(item: Option<Data>) -> Option<Data> {
    item.map(|d| Data {
        value: d.value.to_uppercase(),
    })
}

#[wasm_bindgen]
pub fn process_many(items: Vec<Data>) -> Vec<Data> {
    items
        .into_iter()
        .map(|d| Data {
            value: d.value.to_uppercase(),
        })
        .collect()
}

Error Handling

The generated code includes automatic error handling:
// From bomboni_wasm_derive/src/wasm.rs
fn expand_wasm_error_handler(ident: &syn::Ident) -> TokenStream {
    quote! {
        {
            let err: JsValue = err.into();
            if let Some(err_str) = err.as_string() {
                _wasm_bindgen::throw_str(
                    &format!("error converting from WASM `{}`: {}",
                        stringify!(#ident),
                        err_str,
                    )
                )
            } else {
                _wasm_bindgen::throw_val(err)
            }
        }
    }
}
Errors during conversion are automatically caught and rethrown with context.

Type Conversions

The Wasm Trait

All types with the Wasm derive implement the Wasm trait:
pub trait Wasm {
    type JsType: JsCast;

    fn to_js(&self) -> Result<Self::JsType, serde_wasm_bindgen::Error>
    where
        Self: serde::Serialize,
    {
        self.serialize(&JSON_SERIALIZER)
            .map(JsCast::unchecked_from_js)
    }

    fn from_js<T: Into<JsValue>>(js: T) -> Result<Self, serde_wasm_bindgen::Error>
    where
        Self: serde::de::DeserializeOwned,
    {
        serde_wasm_bindgen::from_value(js.into())
    }
}

Manual Conversions

You can manually convert between Rust and JavaScript:
use wasm_bindgen::prelude::*;
use bomboni_wasm::Wasm;

#[derive(Serialize, Deserialize, Wasm)]
pub struct Message {
    pub text: String,
}

#[wasm_bindgen]
pub fn process_message(js_msg: JsValue) -> Result<JsValue, JsValue> {
    // Convert from JavaScript to Rust
    let mut msg = Message::from_js(js_msg)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    
    // Process in Rust
    msg.text = msg.text.to_uppercase();
    
    // Convert from Rust to JavaScript
    msg.to_js()
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

Serialization Format

Bomboni uses serde-wasm-bindgen with JSON-compatible serialization:
const JSON_SERIALIZER: serde_wasm_bindgen::Serializer =
    serde_wasm_bindgen::Serializer::json_compatible();
This ensures that:
  • Maps serialize to JavaScript objects (not Map)
  • Numbers use appropriate JavaScript number types
  • Strings, booleans, and arrays work as expected

Complete Example

Here’s a complete example showing various interoperability patterns:
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use bomboni_wasm::{Wasm, console_log, console_error};
use std::collections::HashMap;

// Standard struct with automatic conversion
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(into_wasm_abi, from_wasm_abi)]
pub struct User {
    pub id: u32,
    pub name: String,
}

// String-backed ID type
#[derive(Clone, Wasm)]
#[wasm(js_value(convert_string))]
pub struct UserId(String);

impl std::fmt::Display for UserId {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl std::str::FromStr for UserId {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(UserId(s.to_string()))
    }
}

// Custom JsValue conversion
#[derive(Wasm)]
#[wasm(js_value(
    into = Timestamp::to_js,
    try_from = Timestamp::from_js,
))]
pub struct Timestamp(i64);

impl Timestamp {
    fn to_js(self) -> JsValue {
        js_sys::Date::new(&JsValue::from_f64(self.0 as f64)).into()
    }
    
    fn from_js(js: JsValue) -> Result<Self, JsValue> {
        if let Some(date) = js.dyn_ref::<js_sys::Date>() {
            Ok(Timestamp(date.get_time() as i64))
        } else {
            Err(JsValue::from_str("Expected a Date object"))
        }
    }
}

// WASM API functions
#[wasm_bindgen]
pub fn create_user(id: u32, name: String) -> Result<User, JsValue> {
    console_log!("Creating user: {} ({})", name, id);
    Ok(User { id, name })
}

#[wasm_bindgen]
pub fn get_user_by_id(user_id: UserId) -> Result<User, JsValue> {
    console_log!("Looking up user: {}", user_id);
    // Lookup logic...
    Ok(User {
        id: 1,
        name: "Alice".to_string(),
    })
}

#[wasm_bindgen]
pub fn process_users(users: Vec<User>) -> Result<JsValue, JsValue> {
    console_log!("Processing {} users", users.len());
    
    let mut map = HashMap::new();
    for user in users {
        map.insert(user.id.to_string(), user.name);
    }
    
    serde_wasm_bindgen::to_value(&map)
        .map_err(|e| {
            console_error!("Serialization error: {}", e);
            JsValue::from_str(&e.to_string())
        })
}
TypeScript usage:
import init, {
  User,
  UserId,
  create_user,
  get_user_by_id,
  process_users
} from './pkg/your_crate';

await init();

// Standard struct
const user: User = create_user(1, "Alice");

// String-backed ID
const userId: string = "user-123";
const foundUser = get_user_by_id(userId);

// Vector processing
const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];
const userMap = process_users(users);

Build docs developers (and LLMs) love