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

Add lightweight wasm support to rust-decimal #650

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,19 @@ jobs:
name:
- stable
- beta
- stable / wasm
- beta / wasm
include:
# This allows us to define targets in the future (e.g. wasm32-unknown-unknown)
- name: stable
rust: stable
- name: beta
rust: beta
- name: stable / wasm
rust: stable
target: wasm32-unknown-unknown
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: We need to make the tests adhere to this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(So the pass in the CI is not accurate)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking a bit further into this, I think this is the wrong approach. I believe a wasm target needs to be handled completely separately.

- name: beta / wasm
rust: beta
target: wasm32-unknown-unknown
steps:
- uses: actions/checkout@v4

Expand All @@ -148,11 +155,6 @@ jobs:

- uses: davidB/rust-cargo-make@v1

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get -y install libpq-dev

- name: Run no_std tests
run: cargo make test-no-std
env:
Expand Down Expand Up @@ -187,12 +189,19 @@ jobs:
name:
- stable
- beta
- stable / wasm
- beta / wasm
include:
# This allows us to define targets in the future (e.g. wasm32-unknown-unknown)
- name: stable
rust: stable
- name: beta
rust: beta
- name: stable / wasm
rust: stable
target: wasm32-unknown-unknown
- name: beta / wasm
rust: beta
target: wasm32-unknown-unknown
steps:
- uses: actions/checkout@v4

Expand All @@ -211,11 +220,6 @@ jobs:

- uses: davidB/rust-cargo-make@v1

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get -y install libpq-dev

- name: Run serde tests
run: cargo make test-serde
env:
Expand Down
29 changes: 19 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
authors = ["Paul Mason <paul@form1.co.nz>"]
build = "build.rs"
categories = ["science","mathematics","data-structures"]
categories = ["science", "mathematics", "data-structures"]
description = "Decimal number implementation written in pure Rust suitable for financial and fixed-precision calculations."
documentation = "https://docs.rs/rust_decimal/"
edition = "2021"
exclude = [ "tests/generated/*" ]
keywords = ["decimal","financial","fixed","precision","number"]
exclude = ["tests/generated/*"]
keywords = ["decimal", "financial", "fixed", "precision", "number"]
license = "MIT"
name = "rust_decimal"
readme = "./README.md"
Expand All @@ -17,6 +17,7 @@ version = "1.35.0"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]

[dependencies]
arbitrary = { default-features = false, optional = true, version = "1.0" }
Expand All @@ -37,19 +38,27 @@ serde = { default-features = false, optional = true, version = "1.0" }
serde_json = { default-features = false, optional = true, version = "1.0" }
tokio-postgres = { default-features = false, optional = true, version = "0.7" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { default-features = false, version = "0.2" }
Comment on lines +41 to +42
Copy link
Contributor

@robjtede robjtede Apr 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good idea to declare these things as optional even with the cfg guard, including wasm-bindgen/js-sys. Chrono found this out and reverted it: chronotope/chrono#1164. There's some nasty circular dep situations that can occur otherwise.

In their case it may have been due to it being a default-on feature; I admit I havent checkout out this branch and run cargo tree, but it's worth investigating for a core-ish datatype crate such as this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out and thank you for raising this @robjtede

Admittedly, I'm still getting up to speed on this so it's good to find out what not to do also!


[dev-dependencies]
bincode = { default-features = false, version = "1.0" }
bytes = { default-features = false, version = "1.0" }
criterion = { default-features = false, version = "0.5" }
csv = "1"
futures = { default-features = false, version = "0.3" }
rand = { default-features = false, features = ["getrandom"], version = "0.8" }
rust_decimal_macros = { default-features = false, version = "1.33" }
serde = { default-features = false, features = ["derive"], version = "1.0" }
serde_json = "1.0"
tokio = { default-features = false, features = ["macros", "rt-multi-thread", "test-util"], version = "1.0" }
version-sync = { default-features = false, features = ["html_root_url_updated", "markdown_deps_updated"], version = "0.9" }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen = { default-features = false, version = "0.2" }

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
bincode = { default-features = false, version = "1.0" }
bytes = { default-features = false, version = "1.0" }
futures = { default-features = false, version = "0.3" }
rand = { default-features = false, features = ["getrandom"], version = "0.8" }
postgres = { default-features = false, version = "0.19" }
tokio = { default-features = false, features = ["macros", "rt-multi-thread", "test-util"], version = "1.0" }
tokio-postgres = { default-features = false, version = "0.7" }

[features]
Expand Down Expand Up @@ -77,15 +86,15 @@ rkyv = ["dep:rkyv"]
rkyv-safe = ["rkyv/validation"]
rocket-traits = ["dep:rocket"]
rust-fuzz = ["dep:arbitrary"]
serde = ["dep:serde"]
serde = ["dep:serde", "wasm-bindgen/serde"]
serde-arbitrary-precision = ["serde-with-arbitrary-precision"]
serde-bincode = ["serde-str"] # Backwards compatability
serde-float = ["serde-with-float"]
serde-str = ["serde-with-str"]
serde-with-arbitrary-precision = ["serde", "serde_json/arbitrary_precision", "serde_json/std"]
serde-with-float = ["serde"]
serde-with-str = ["serde"]
std = ["arrayvec/std", "borsh?/std", "bytes?/std", "rand?/std", "rkyv?/std", "serde?/std", "serde_json?/std"]
std = ["arrayvec/std", "wasm-bindgen/std", "borsh?/std", "bytes?/std", "rand?/std", "rkyv?/std", "serde?/std", "serde_json?/std"]
tokio-pg = ["db-tokio-postgres"] # Backwards compatability

[[bench]]
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ assert_eq!(total, dec!(27.26));
* [rocket-traits](#rocket-traits)
* [rust-fuzz](#rust-fuzz)
* [std](#std)
* [wasm](#wasm)

**Database**

Expand Down Expand Up @@ -331,6 +332,12 @@ Please see the `examples` directory for more information regarding `serde_json`
Enable `std` library support. This is enabled by default, however in the future will be opt in. For now, to support `no_std`
libraries, this crate can be compiled with `--no-default-features`.

### `wasm`

Enable [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) support which makes `Decimal` compatible with the
`wasm_bindgen` attribute macro and exposes `fromNumber()` and `toNumber()` methods to convert between `Decimal` and
the primitive `number` type across boundaries.

## Building

Please refer to the [Build document](BUILD.md) for more information on building and testing Rust Decimal.
Expand Down
62 changes: 54 additions & 8 deletions make/tests.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,77 @@ extend = [
{ path = "tests/serde.toml" }
]

[tasks.test-runner]
private = true
env_scripts = [
'''
#!@duckscript
target_value = get_env CI_DECIMAL_TEST_TARGET
target_set = is_set ${target_value}
empty = is_empty ${value}
if target_set and not empty
set_env TEST_TARGET ${target_value}
end
'''
]
dependencies = [
"test-runner-default",
"test-runner-ci-target",
"test-feature-runner-default",
"test-feature-runner-ci-target"
]

[tasks.test-runner-default]
private = true
condition = { env_not_set = ["TEST_FEATURES", "TEST_TARGET"] }
command = "cargo"
args = ["test", "--workspace", "--no-default-features", "${TEST_FILTER}"]

[tasks.test-runner-ci-target]
private = true
condition = { env_not_set = ["TEST_FEATURES"], env_set = ["TEST_TARGET"] }
command = "cargo"
args = ["test", "--workspace", "--no-default-features", "--target=${TEST_TARGET}", "${TEST_FILTER}"]

[tasks.test-feature-runner-default]
private = true
condition = { env_set = ["TEST_FEATURES"], env_not_set = ["TEST_TARGET"] }
command = "cargo"
args = ["test", "--workspace", "--no-default-features", "--features=${TEST_FEATURES}", "${TEST_FILTER}"]

[tasks.test-feature-runner-ci-target]
private = true
condition = { env_set = ["TEST_FEATURES", "TEST_TARGET"] }
command = "cargo"
args = ["test", "--workspace", "--no-default-features", "--features=${TEST_FEATURES}", "--target=${TEST_TARGET}", "${TEST_FILTER}"]

[tasks.test]
clear = true
dependencies = ["test-no-std", "test-default"]

# Some tests need cleaning before hand to ensure we don't inadvertantly test
# Some tests need cleaning beforehand to ensure we don't inadvertantly test
# using prebuilt logic
[tasks.clean-no-std]
alias = "clean"

[tasks.test-no-std]
dependencies = ["clean-no-std"]
command = "cargo"
args = ["test", "--no-default-features"]
dependencies = [
"clean-no-std"
]
env = { TEST_FILTER = "" }
run_task = "test-runner"

[tasks.clean-default]
alias = "clean"

[tasks.test-default]
dependencies = ["clean-default"]
command = "cargo"
args = ["test", "--workspace", "--features=default"]
env = { TEST_FEATURES = "default", TEST_FILTER = "" }
run_task = "test-runner"

[tasks.test-legacy-ops]
command = "cargo"
args = ["test", "--workspace", "--features=legacy-ops"]
env = { TEST_FEATURES = "legacy-ops", TEST_FILTER = "" }
run_task = "test-runner"

# This should reflect the steps in github
[tasks.test-all]
Expand Down
28 changes: 14 additions & 14 deletions make/tests/misc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,29 @@ dependencies = [
]

[tasks.test-proptest]
command = "cargo"
args = ["test", "--workspace", "--no-default-features", "--features=proptest", "proptest_tests", "--", "--skip", "generated"]
env = { TEST_FEATURES = "proptest", TEST_FILTER = "proptest_tests" }
run_task = "test-runner"

[tasks.test-rust-fuzz]
command = "cargo"
args = ["test", "--workspace", "--no-default-features", "--features=rust-fuzz", "rust_fuzz_tests", "--", "--skip", "generated"]
env = { TEST_FEATURES = "rust-fuzz", TEST_FILTER = "rust_fuzz_tests" }
run_task = "test-runner"

[tasks.test-rocket-traits]
command = "cargo"
args = ["test", "--workspace", "--features=rocket-traits", "rocket_tests"]
env = { TEST_FEATURES = "rocket-traits", TEST_FILTER = "rocket_tests" }
run_task = "test-runner"

[tasks.test-borsh]
command = "cargo"
args = ["test", "--workspace", "--features=borsh", "borsh_tests", "--", "--skip", "generated"]
env = { TEST_FEATURES = "borsh", TEST_FILTER = "borsh_tests" }
run_task = "test-runner"

[tasks.test-ndarray]
command = "cargo"
args = ["test", "--workspace", "--features=ndarray", "nd_array_tests", "--", "--skip", "generated"]
env = { TEST_FEATURES = "ndarray", TEST_FILTER = "ndarray_tests" }
run_task = "test-runner"

[tasks.test-rkyv]
command = "cargo"
args = ["test", "--workspace", "--features=rkyv", "--features=rkyv-safe", "rkyv_tests", "--", "--skip", "generated"]
env = { TEST_FEATURES = "rkyv", TEST_FILTER = "rkyv_tests" }
run_task = "test-runner"

[tasks.test-rand]
command = "cargo"
args = ["test", "--workspace", "--features=rand", "rand_tests", "--", "--skip", "generated"]
env = { TEST_FEATURES = "rand", TEST_FILTER = "rand_tests" }
run_task = "test-runner"
3 changes: 3 additions & 0 deletions src/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ use num_traits::float::FloatCore;
use num_traits::{FromPrimitive, Num, One, Signed, ToPrimitive, Zero};
#[cfg(feature = "rkyv")]
use rkyv::{Archive, Deserialize, Serialize};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::wasm_bindgen;

/// The smallest value that can be represented by this decimal type.
const MIN: Decimal = Decimal {
Expand Down Expand Up @@ -121,6 +123,7 @@ pub struct UnpackedDecimal {
archive_attr(derive(Clone, Copy, Debug))
)]
#[cfg_attr(feature = "rkyv-safe", archive(check_bytes))]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub struct Decimal {
// Bits 0-15: unused
// Bits 16-23: Contains "e", a value between 0-28 that indicates the scale
Expand Down
24 changes: 16 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ mod arithmetic_impls;
mod fuzz;
#[cfg(feature = "maths")]
mod maths;
#[cfg(any(feature = "db-diesel1-mysql", feature = "db-diesel2-mysql"))]
#[cfg(all(
any(feature = "db-diesel1-mysql", feature = "db-diesel2-mysql"),
not(target_arch = "wasm32")
))]
mod mysql;
#[cfg(any(
feature = "db-tokio-postgres",
feature = "db-postgres",
feature = "db-diesel1-postgres",
feature = "db-diesel2-postgres",
#[cfg(all(
any(
feature = "db-tokio-postgres",
feature = "db-postgres",
feature = "db-diesel1-postgres",
feature = "db-diesel2-postgres",
),
not(target_arch = "wasm32")
))]
mod postgres;
#[cfg(feature = "proptest")]
Expand Down Expand Up @@ -52,6 +58,8 @@ mod serde;
)
))]
pub mod serde;
#[cfg(target_arch = "wasm32")]
pub mod wasm;

pub use decimal::{Decimal, RoundingStrategy};
pub use error::Error;
Expand All @@ -72,11 +80,11 @@ pub mod prelude {
// pub use rust_decimal_macros::dec;
}

#[cfg(all(feature = "diesel1", not(feature = "diesel2")))]
#[cfg(all(feature = "diesel1", not(feature = "diesel2"), not(target_arch = "wasm32")))]
#[macro_use]
extern crate diesel1 as diesel;

#[cfg(feature = "diesel2")]
#[cfg(all(feature = "diesel2", not(target_arch = "wasm32")))]
extern crate diesel2 as diesel;

/// Shortcut for `core::result::Result<T, rust_decimal::Error>`. Useful to distinguish
Expand Down
26 changes: 26 additions & 0 deletions src/wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use num_traits::{FromPrimitive, ToPrimitive};
use wasm_bindgen::prelude::*;

use crate::Decimal;

#[wasm_bindgen]
impl Decimal {
/// Returns a new `Decimal` object instance by converting a primitive number.
#[wasm_bindgen(js_name = fromNumber)]
#[must_use]
pub fn from_number(value: f64) -> Option<Decimal> {
Decimal::from_f64(value)
}

/// Returns the value of this `Decimal` converted to a primitive number.
///
/// # Caution
/// At the time of writing this implementation the conversion from `Decimal` to `f64` cannot
/// fail. To prevent undefined behavior in case the underlying implementation changes `f64::NAN`
/// is returned as a stable fallback value.
#[wasm_bindgen(js_name = toNumber)]
#[must_use]
pub fn to_number(&self) -> f64 {
self.to_f64().unwrap_or(f64::NAN)
}
}
Loading