Deploying Non-Rust WASM Contracts
While Rust provides the best developer experience for Stylus, any language that compiles to WebAssembly can be deployed. This guide explains how to deploy WASM contracts written in C, C++, or even pure WebAssembly Text (WAT).
Overview
Stylus accepts any valid WebAssembly module that meets its requirements. You can:
- Write contracts in C or C++ using the Stylus C SDK
- Use WebAssembly Text (WAT) for direct bytecode control
- Compile from any language that targets
wasm32-unknown-unknown - Deploy pre-compiled WASM binaries directly
The key is using the --wasm-file flag with cargo stylus commands to bypass Rust compilation.
Why Use Non-Rust Languages?
Different languages excel at different tasks:
| Language | Best For | Use Cases |
|---|---|---|
| C/C++ | Low-level control, cryptography | Hash functions, signature verification, algorithms |
| WAT | Learning, debugging, minimal contracts | Simple logic, educational examples |
| AssemblyScript | TypeScript developers | Web3 integration with familiar syntax |
| Other | Specific requirements | Domain-specific computations |
When to choose non-Rust
- Existing codebase: Port existing C/C++ cryptography libraries
- Performance-critical: Hand-optimized assembly-like control
- Minimal size: Ultra-compact contracts for specific operations
- Team expertise: Leverage existing C/C++ knowledge
When to stick with Rust
- Full-featured contracts: Complex DeFi, NFTs, governance
- Type safety: Strong guarantees and tooling
- Ecosystem: Rich library support and examples
- Productivity: Higher-level abstractions and macros
WASM Requirements
All WASM modules deployed to Stylus must meet these requirements:
Required exports
(export "user_entrypoint" (func $user_entrypoint))
(export "memory" (memory 0))
The user_entrypoint function:
- Signature:
(param i32) (result i32) - Parameter: Length of input calldata in bytes
- Returns: Length of output data in bytes
Allowed imports
Only functions from the vm_hooks module are permitted:
(import "vm_hooks" "msg_sender" (func $msg_sender (param i32)))
(import "vm_hooks" "storage_load_bytes32" (func $storage_load (param i32 i32)))
(import "vm_hooks" "storage_store_bytes32" (func $storage_store (param i32 i32)))
See the hostio exports documentation for the complete list of available VM hooks.
Memory requirements
- Linear memory must be exported as
"memory" - Memory growth must be explicitly paid for
- Initial memory size should be minimal (often
0 0) - Maximum memory is limited by gas costs
Compilation target
- Target triple:
wasm32-unknown-unknown - No standard library: WASM runs in a sandboxed environment
- No floating point: Not yet supported by Stylus
- No SIMD: Not yet supported by Stylus
- No reference types: Disabled for compatibility
WebAssembly Text (WAT)
WAT provides direct control over WASM bytecode using human-readable text format.
Minimal contract
The simplest valid Stylus contract:
(module
;; Export linear memory
(memory 0 0)
(export "memory" (memory 0))
;; Required entrypoint
;; Takes calldata length, returns output length
(func (export "user_entrypoint") (param $args_len i32) (result i32)
(i32.const 0) ;; Return 0 bytes
))
Save as minimal.wat and deploy:
cargo stylus deploy --wasm-file=minimal.wat --private-key-path=./key.txt
Echo contract
Returns input data unchanged:
(module
(memory 1 1)
(export "memory" (memory 0))
;; Import VM hook to read calldata
(import "vm_hooks" "read_args" (func $read_args (param i32)))
(func (export "user_entrypoint") (param $args_len i32) (result i32)
;; Read calldata into memory at offset 0
(call $read_args (i32.const 0))
;; Return the same length (echo)
(local.get $args_len)
))
Storage counter
Increment a value in storage:
(module
(memory 1 1)
(export "memory" (memory 0))
;; Import storage operations
(import "vm_hooks" "storage_load_bytes32"
(func $storage_load (param i32 i32)))
(import "vm_hooks" "storage_store_bytes32"
(func $storage_store (param i32 i32)))
(func (export "user_entrypoint") (param $args_len i32) (result i32)
;; Load current value from storage slot 0
(call $storage_load
(i32.const 0) ;; key pointer
(i32.const 32)) ;; value destination
;; Increment the value at memory[32]
(i32.store (i32.const 32)
(i32.add
(i32.load (i32.const 32))
(i32.const 1)))
;; Store back to storage
(call $storage_store
(i32.const 0) ;; key pointer
(i32.const 32)) ;; value pointer
;; Return 0 bytes of output
(i32.const 0)
))
Checking WAT contracts
Validate before deploying:
cargo stylus check --wasm-file=counter.wat
Output shows validation results:
Reading WASM file at counter.wat
Compressed WASM size: 142 B
Contract succeeded Stylus onchain activation checks with Stylus version: 1
C/C++ Development
The Stylus C SDK enables C/C++ smart contract development.
Installation
Install the C SDK:
git clone https://github.com/OffchainLabs/stylus-sdk-c.git
cd stylus-sdk-c
Install dependencies:
# macOS
brew install llvm binaryen wabt
# Ubuntu/Debian
sudo apt-get install clang lld wasm-ld binaryen wabt
Project structure
Basic C project layout:
my-contract/
├── Makefile
├── src/
│ └── main.c
└── include/
└── stylus_sdk.h
Simple C contract
// main.c
#include "stylus_sdk.h"
// Storage slot for counter
static uint8_t counter_slot[32] = {0};
// Main entrypoint
int main(int argc, char *argv[]) {
// Load counter from storage
uint8_t value[32];
storage_load_bytes32(counter_slot, value);
// Increment
value[31]++;
// Store back
storage_store_bytes32(counter_slot, value);
return 0;
}
C SDK features
The C SDK provides:
// Account operations
void msg_sender(uint8_t *sender);
void tx_origin(uint8_t *origin);
void contract_address(uint8_t *addr);
// Storage operations
void storage_load_bytes32(uint8_t *key, uint8_t *dest);
void storage_store_bytes32(uint8_t *key, uint8_t *value);
// Block information
uint64_t block_timestamp(void);
uint64_t block_number(void);
void block_basefee(uint8_t *basefee);
// Call operations
void call_contract(
uint8_t *contract,
uint8_t *calldata,
uint32_t calldata_len,
uint8_t *value,
uint32_t gas,
uint8_t *return_data_len
);
// And many more...
Building C contracts
Create a Makefile:
CLANG = clang
WASM_LD = wasm-ld
WASM_OPT = wasm-opt
CFLAGS = -target wasm32 -nostdlib -O3
LDFLAGS = -no-entry --export=user_entrypoint --export=memory
SRC = src/main.c
OUT = build/contract.wasm
OUT_OPT = build/contract-opt.wasm
all: $(OUT_OPT)
$(OUT): $(SRC)
mkdir -p build
$(CLANG) $(CFLAGS) -c $(SRC) -o build/main.o
$(WASM_LD) $(LDFLAGS) build/main.o -o $(OUT)
$(OUT_OPT): $(OUT)
$(WASM_OPT) -Oz $(OUT) -o $(OUT_OPT)
clean:
rm -rf build
deploy: $(OUT_OPT)
cargo stylus deploy --wasm-file=$(OUT_OPT) \
--private-key-path=$$PRIVATE_KEY_PATH
check: $(OUT_OPT)
cargo stylus check --wasm-file=$(OUT_OPT)
Build and deploy:
make
make check
make deploy
C cryptography example
Verifying a signature:
#include "stylus_sdk.h"
#include <string.h>
// Verify ECDSA signature
int verify_signature(
uint8_t *message_hash,
uint8_t *signature,
uint8_t *public_key
) {
uint8_t recovered[65];
// Recover signer from signature
if (ecrecover(message_hash, signature, recovered) != 0) {
return -1; // Recovery failed
}
// Compare with expected public key
if (memcmp(recovered + 1, public_key, 64) == 0) {
return 0; // Valid signature
}
return -1; // Invalid signature
}
int main(int argc, char *argv[]) {
uint8_t msg_hash[32];
uint8_t sig[65];
uint8_t pubkey[64];
// Read inputs from calldata
read_args(0);
memcpy(msg_hash, memory, 32);
memcpy(sig, memory + 32, 65);
memcpy(pubkey, memory + 97, 64);
// Verify
int result = verify_signature(msg_hash, sig, pubkey);
// Write result
memory[0] = (result == 0) ? 1 : 0;
write_result(memory, 1);
return 0;
}
AssemblyScript Contracts
AssemblyScript is TypeScript-like language that compiles to WebAssembly.
Installation
npm install -g assemblyscript
npm install @assemblyscript/loader
Simple AssemblyScript contract
// contract.ts
// Import Stylus VM hooks
@external("vm_hooks", "msg_sender")
declare function msg_sender(ptr: usize): void;
@external("vm_hooks", "storage_load_bytes32")
declare function storage_load(key: usize, dest: usize): void;
@external("vm_hooks", "storage_store_bytes32")
declare function storage_store(key: usize, value: usize): void;
// Storage key
const COUNTER_KEY: StaticArray<u8> = [0, 0, 0, 0, /* ... 32 zeros ... */];
// Entrypoint
export function user_entrypoint(args_len: i32): i32 {
// Load counter
let value = new StaticArray<u8>(32);
storage_load(
changetype<usize>(COUNTER_KEY),
changetype<usize>(value)
);
// Increment
value[31]++;
// Store
storage_store(
changetype<usize>(COUNTER_KEY),
changetype<usize>(value)
);
return 0; // No output
}
Compile AssemblyScript
asc contract.ts \
--target release \
--exportRuntime \
--exportTable \
-o contract.wasm
Deploy AssemblyScript contract
cargo stylus deploy \
--wasm-file=contract.wasm \
--private-key-path=./key.txt
Deployment Workflow
1. Prepare your WASM
Ensure your WASM module meets requirements:
# Check WASM structure with wasm-objdump
wasm-objdump -x contract.wasm | grep -A 5 "Export\|Import"
# Should show:
# Export[0]:
# - func[0] <user_entrypoint>
# - memory[0]
# Import[0]:
# - module="vm_hooks" func=...