Overview
wacore is the core WhatsApp protocol implementation for whatsapp-rust. It’s designed to be platform-agnostic with no runtime dependencies on Tokio or specific databases, making it portable across different async runtimes and storage backends.
Philosophy
wacore contains all the core logic for:- Binary protocol encoding/decoding
- Cryptographic primitives (AES-GCM, Signal Protocol)
- IQ protocol types and specifications
- Runtime abstraction (
Runtimetrait for pluggable async executors) - Network abstractions (
Transport,TransportFactory,HttpClienttraits) - State management traits (
Backend,SignalStore,AppSyncStore, etc.) - Message builders and parsers
futures, async-trait, async-lock, and async-channel for async primitives. This makes wacore portable to any async runtime, including WASM targets. The main whatsapp-rust crate provides concrete implementations (Tokio runtime, SQLite storage, ureq HTTP client, Tokio WebSocket transport).
Key Exports
Re-exported Crates
Derive Macros
EmptyNode- For protocol nodes with only a tag (no attributes)ProtocolNode- For protocol nodes with string attributesStringEnum- For enums with string representations
Framing
Core Modules
Protocol & Binary
Binary protocol
Type-safe protocol node builders and parsers
xml
XML utilities for protocol nodes
Cryptography
Signal Protocol
Signal Protocol implementation for E2E encryption
noise
Noise Protocol XX for handshake encryption
IQ Protocol
blocklist- Block/unblock contactschatstate- Typing indicators, presencecontacts- Contact synchronizationdirty- Dirty bit checkinggroups- Group management operationskeepalive- Connection keepalivemediaconn- Media server connectionsmex- Message Extension queriespassive- Passive IQ handlingprekeys- Prekey distributionprivacy- Privacy settingsprops- Server properties and A/B experiment configs (includesconfig_codesfor well-known experiment flags)spam_report- Spam reportingtctoken- Temporary client tokensusync- User synchronization
Message Handling
messages
Message encryption and decryption
send
Message sending logic
download
Media download and decryption
upload
Media encryption and upload preparation
State Management
Device- Core device state structureDeviceCommand- State mutation commandstraits- Backend trait definitions (Backend,SessionStore, etc.)ab_props- In-memory A/B experiment property cache (AbPropsCache), populated fromfetch_props()on each connection. Features query this cache to check server-side experiment flags (e.g., privacy token attachment on group operations).
Runtime & Networking
runtime module defines the Runtime trait that all async operations go through:
| Method | Purpose |
|---|---|
spawn | Spawn a background task, returning an AbortHandle for cancellation. The handle is #[must_use] — dropping it aborts the task. Call .detach() for fire-and-forget tasks |
sleep | Return a future that completes after a duration |
spawn_blocking | Offload a blocking closure to a thread pool |
yield_now | Cooperatively yield; return None if unnecessary (e.g., multi-threaded runtimes) |
yield_frequency | How many items to process before yielding in tight loops (default: 10) |
Helper functions
Theruntime module also provides two runtime-agnostic helper functions that work with any Runtime implementation:
timeout — Race a future against a deadline using the runtime’s sleep implementation:
Ok(value) if the future completes before the duration, or Err(Elapsed) if it times out. This is the runtime-agnostic replacement for tokio::time::timeout — the library uses it internally for phash validation, media retry timeouts, session establishment, and IQ response waiting.
blocking — Offload a blocking closure and return its result:
Runtime::spawn_blocking with a oneshot channel to ferry the closure’s return value back to the caller. On WASM, the closure runs inline since there is only one thread.
The net module defines the networking abstractions:
Transport— active connection for sending/receiving raw bytesTransportFactory— creates new transport instances and event streamsHttpClient— HTTP request execution (buffered and streaming)TransportEvent—Connected,DataReceived(Bytes),Disconnected
target_arch = "wasm32"), all Send bounds are automatically removed.
Connection & Pairing
handshake
Noise Protocol handshake
pair
QR code pairing
pair_code
Phone number pairing
net
Transport and HTTP client traits
Specialized Features
appstate
App state synchronization (contacts, settings)
history_sync
Message history synchronization
usync
User device list synchronization
prekeys
Signal Protocol prekey generation
Time
time module centralizes all timestamp handling. By default it uses chrono::Utc::now(), but you can override the provider globally via set_time_provider for environments where std::time::SystemTime is unavailable (e.g., WASM) or for deterministic testing.
Functions
| Function | Return type | Description |
|---|---|---|
now_millis() | i64 | Current time in milliseconds since Unix epoch |
now_secs() | i64 | Current time in seconds since Unix epoch |
now_utc() | DateTime<Utc> | Current time as a chrono::DateTime<Utc> |
from_secs(ts) | Option<DateTime<Utc>> | Convert Unix timestamp (seconds) to DateTime<Utc> |
from_secs_or_now(ts) | DateTime<Utc> | Like from_secs, falling back to now_utc() for out-of-range values |
from_millis(ts) | Option<DateTime<Utc>> | Convert Unix timestamp (milliseconds) to DateTime<Utc> |
from_millis_or_now(ts) | DateTime<Utc> | Like from_millis, falling back to now_utc() for out-of-range values |
Custom time provider
Implement theTimeProvider trait and call set_time_provider before any time functions are used:
set_time_provider returns Err if a provider has already been set. The provider uses OnceLock internally, so it can only be configured once per process.Instant
The module also provides a portableInstant type that replaces std::time::Instant, which is unavailable on wasm32-unknown-unknown. It uses now_millis() internally — not truly monotonic, but sufficient for elapsed-time measurement and timeout tracking.
Instant supports Add<Duration> and Sub<Instant> (returning Duration), with saturating arithmetic to prevent overflow.
Utilities
client- Client context traitsib- Identity byte utilitiesproto_helpers- Protobuf conversion helpersreporting_token- Reporting token generationrequest- Request building utilitiesstanza- Common stanza builderssticker_pack- Sticker pack creation helpers (see sticker packs)time- Pluggable time provider with portableInstant(see time above)types- Common type definitions (JID, events, messages). Includestypes::jidutilities for zero-allocation JID comparison (cmp_for_lock_order), buffer-reusing address formatting (write_protocol_address_to), and in-place sorted deduplication (sort_dedup_by_user,sort_dedup_by_device)version- WhatsApp version constantswebp- WebP format utilities (animated sticker detection)
webp module
Thewebp module provides utilities for working with WebP image files. It is re-exported as whatsapp_rust::webp.
is_animated
Detects whether a WebP file contains animation frames by parsing RIFF/VP8X headers and scanning for ANIM/ANMF chunks.Raw WebP file bytes.
true if the WebP file is animated, false otherwise (including for invalid or too-short input).
Example:
This function is used internally by
create_sticker_pack_zip to set the is_animated field on each sticker proto entry. You can also use it directly when you need to classify WebP files before processing.Submodule Packages
wacore is split into several workspace crates:wacore-binary
Location:wacore/binary
Binary protocol encoding/decoding using WhatsApp’s custom format.
CompactString- Re-export ofcompact_str::CompactString, used byJid.user,NodeValue::String, andNodeContent::Stringjid::{Jid, JidRef, Server, JidExt}- WhatsApp JID types.Serveris an enum (#[repr(u8)]) with variants for all known WhatsApp server domains (Pn,Lid,Group,Broadcast,Newsletter,Hosted,HostedLid,Messenger,Interop,Bot,Legacy).JidExtprovides helper methods (is_group(),is_newsletter(), etc.) for both owned and borrowed JID typesnode::{Node, NodeRef, NodeStr, NodeValue, ValueRef, OwnedNodeRef}- Protocol node types.Node(owned) for building outgoing stanzas,NodeRef(borrowed) for reading received stanzas,NodeStrfor borrowed-or-inline decoded strings,OwnedNodeReffor yoke-based zero-copy self-referential nodes shared asArc<OwnedNodeRef>. The entireNodeReftype family (NodeRef,NodeStr,ValueRef,JidRef,NodeContentRef,OwnedNodeRef) implementsserde::Serialize, producing output identical to their owned counterparts — enabling zero-copy serialization without converting toNodefirstbuilder::NodeBuilder- Fluent node builder withnew(&'static str)/new_dynamic(String),attr(),jid_attr(),children(),bytes(),string_content(), andapply_content()chaining methodsmarshal::*- Binary marshaling functionsattrs::{AttrParser, AttrParserRef}- Attribute parsing utilities for ownedNodeand borrowedNodeRefrespectivelytoken- Token dictionary
wacore-libsignal
Location:wacore/libsignal
Signal Protocol implementation for end-to-end encryption.
core- Core session cipher logiccrypto- Cryptographic primitives (HKDF, HMAC, AES)protocol- Protocol message typesstore- Store trait definitions
wacore-noise
Location:wacore/noise
Noise Protocol XX implementation for handshake encryption.
NoiseState- Generic Noise XX state machineNoiseHandshake- WhatsApp-specific handshake wrapperHandshakeUtils- Protocol message building/parsingframing- WebSocket frame encodingbuild_edge_routing_preintro- Edge routing helper
wacore-appstate
Location:wacore/appstate
App state synchronization for contacts, settings, and metadata.
process_snapshot- Process full state snapshotsprocess_patch- Apply incremental patchesMutation- State mutation recordsLTHash- LTHash implementation for integrityexpand_app_state_keys- Key derivation
wacore-derive
Location:wacore/derive
Procedural macros for protocol node generation.
Usage in Main Library
The mainwhatsapp-rust crate uses wacore modules throughout:
Design Principles
Platform-Agnostic
No dependencies on:- Tokio or any async runtime — uses only
futures,async-trait,async-lock,async-channel - Specific database implementations
- File system operations
- Runtime: Tokio (default), async-std, smol, WASM, etc. — implement
Runtime(4 methods) - Storage: SQLite (default), PostgreSQL, in-memory, etc. — implement
Backend(4 sub-traits) - Transport: Tokio WebSocket (default), custom protocols — implement
TransportFactory+Transport - HTTP client: ureq (default), reqwest, surf, etc. — implement
HttpClient
Type Safety
Strong typing throughout:JidwithServerenum for WhatsApp identifiers — server type is an enum variant, not a stringNode/NodeReffor protocol messages (owned / borrowed)- Validated newtypes (e.g.,
GroupSubjectwith length limits) - Enum variants with
StringEnumfor protocol values
Zero-Copy Where Possible
Cow<'static, str>for ownedNode.tagandAttrskeys — known protocol strings (from the token dictionary) are borrowed as static references with zero heap allocation, while unknown strings fall back to ownedStringNodeStr<'a>for borrowedNodeRef.tag,AttrsRefkeys,ValueRef::String, andJidRef.user— a borrowed-or-inline string type where theOwnedvariant usesCompactString(inline up to 24 bytes) instead of heap-allocatedString, reducing allocation pressure during decodingServerenum (#[repr(u8)]) forJid.server— aCopytype that requires zero allocation, replacing the previousCow<'static, str>string-based server fieldOwnedNodeRef— yoke-based self-referential node that owns the decompressed network buffer whileNodeRefborrows string/byte payloads directly from it. Received stanzas flow through the system asArc<OwnedNodeRef>for cheap shared zero-copy access- Zero-copy
Serialize— the entireNodeReftype family (NodeRef,NodeStr,ValueRef,JidRef,NodeContentRef,OwnedNodeRef) implementsserde::Serialize, producing output identical to their owned counterparts. This allows serializing received stanzas directly from the network buffer without converting to ownedNodetypes first. See Binary Protocol — Zero-copy serialization for details NodeReffor borrowed node parsingAttrParserReffor attribute iterationmarshal_reffor encoding without cloning
Benchmarks
The project includes two categories of benchmarks: protocol-level (iai-callgrind, deterministic instruction counts) and integration-level (real client operations with allocation tracking).Protocol benchmarks (iai-callgrind)
wacore includes a suite of iai-callgrind benchmarks that measure instruction counts for core protocol operations. These benchmarks run under Valgrind’s Callgrind tool, producing deterministic, low-noise measurements that are suitable for CI regression tracking.Prerequisites
Running the protocol benchmarks requires:- Nightly Rust (the project pins
nightly-2026-04-05) - Valgrind installed on your system
- iai-callgrind-runner (
cargo install iai-callgrind-runner --version 0.16.1)
Available suites
| Suite | Location | What it measures |
|---|---|---|
send_receive_benchmark | wacore/benches/send_receive_benchmark.rs | Full send/receive pipeline — DM send, DM receive, group send (steady-state skmsg), group send with SKDM distribution (10/50/256 participants), and group receive. Uses real prepare_peer_stanza and prepare_group_stanza functions with in-memory Signal stores |
reporting_token_benchmark | wacore/benches/reporting_token_benchmark.rs | Reporting token generation — key derivation, token calculation, and full generation pipeline for simple and extended messages |
binary_benchmark | wacore/binary/benches/binary_benchmark.rs | Binary protocol encoding/decoding — marshal and unmarshal operations for various node sizes and structures |
libsignal_benchmark | wacore/libsignal/benches/libsignal_benchmark.rs | Signal Protocol operations — session establishment, message encrypt/decrypt, sender key distribution, and group cipher operations |
Running protocol benchmarks
Integration benchmarks
Thebench-integration test suite (tests/bench-integration/) measures real-world client operations end-to-end, including wall-clock time and heap allocation counts. It uses a custom CountingAlloc global allocator that tracks every allocation and byte count, then produces customSmallerIsBetter JSON output for CI trend tracking.
Scenarios
| Scenario | What it measures |
|---|---|
connect_to_ready | Client creation through Connected event — includes transport connect, Noise handshake, and initial sync |
send_message | Single DM send (sender side) — protobuf encoding, Signal encrypt, node marshal, and WebSocket write. Also measures amortized cost over 20 consecutive sends |
send_and_receive_message | Full round-trip — send a DM and wait for delivery on the receiver. Also measures amortized cost over 20 round-trips |
reconnect | Disconnect and reconnect cycle — measures the time and allocations to re-establish a session |
alloc_count (number of heap allocations), alloc_bytes (total bytes allocated), and wall_ms (elapsed wall-clock time). Amortized scenarios divide totals by the number of iterations for per-operation costs.
Running integration benchmarks
Integration benchmarks require a mock WhatsApp server (e.g., Bartender):customSmallerIsBetter entries printed to stdout, suitable for consumption by github-action-benchmark.
Integration benchmarks require the
danger-skip-tls-verify and debug-diagnostics features, which are enabled automatically via the bench-integration crate’s Cargo.toml. You can also enable DHAT heap profiling with --features dhat-heap for detailed allocation flamegraphs.Allocation optimizations
The library includes several allocation-reduction strategies that the integration benchmarks track:- Thread-local zlib pool — The binary protocol decompressor (
decompress_zlib_pooled) reuses a thread-localflate2::Decompressinstance (~48 KB internal state) and output buffer across calls, avoiding per-frame heap allocations CompactStringfor JIDs — JID user fields usecompact_str::CompactStringwhich stores strings up to 24 bytes inline (no heap allocation), covering the vast majority of phone numbers and LID identifiersServerenum — JID server fields use a#[repr(u8)]enum instead of heap-allocated strings, making JID construction and comparison zero-allocation- Zero-copy node decoding — Received stanzas are decoded as
NodeRefborrowing directly from the network buffer viaOwnedNodeRef(yoke-based self-referential type), avoiding cloning string/byte payloads during decode - Pre-allocated buffers — History sync decompression uses a
compressed_size_hintwith a 4x multiplier for buffer pre-allocation, reducingVecreallocation during decompression
CI integration
The repository includes GitHub Actions workflows for both benchmark types: Protocol benchmarks (.github/workflows/benchmark.yml):
- Runs on every push to
mainand on pull requests - Uses github-action-benchmark to store baselines and generate trend charts on GitHub Pages
- A custom parser script (
.github/scripts/iai-to-benchmark-json.py) converts iai-callgrind instruction counts into thecustomSmallerIsBetterJSON format - Pull requests receive a collapsed PR comment (via
.github/scripts/bench-comment.py) grouping benchmarks into regressions (>5% slower), improvements (>5% faster), and unchanged - The PR comment is idempotent — subsequent benchmark runs update the existing comment instead of creating duplicates
.github/workflows/bench-integration.yml):
- Runs on every push to
mainand on pull requests - Spins up a Bartender mock server as a Docker service container
- Measures allocation counts, allocation bytes, and wall-clock time for each scenario
- Pushes to
mainstore the baseline for trend tracking - Pull requests compare against the baseline
Next steps
waproto
Protocol Buffers message definitions
Binary Protocol
Type-safe protocol node pattern
Architecture
IqSpec request/response pairing
State Management
Device state and commands