wasm-pack ile Rust Kodunu npm Paketine Dönüştürme
Rust ile bir şeyler geliştirip bunu JavaScript dünyasına taşımak istediğinizde, karşınıza çıkan en temiz çözüm wasm-pack oluyor. Özellikle performans kritik işlemleri Rust’ta yazıp npm paketi olarak yayınlamak, modern web geliştirme dünyasında giderek yaygınlaşan bir pratik. Bu yazıda sıfırdan başlayıp production’a hazır bir npm paketi oluşturana kadar tüm adımları ele alacağız.
wasm-pack Nedir ve Neden Kullanmalısınız?
wasm-pack, Rust kodunu WebAssembly’ye derleyip npm ekosistemiyle uyumlu paketler oluşturmanıza yarayan bir araç. Mozilla tarafından geliştirilen ve artık topluluk tarafından sürdürülen bu araç, normalde oldukça acı verici olan wasm derleme sürecini ciddi ölçüde basitleştiriyor.
Neden önemli? Şöyle düşünün: Görüntü işleme, kriptografi, sıkıştırma algoritmaları veya ağır hesaplamalar yapıyorsunuz. JavaScript’te bu işlemler için ya yavaş bir native implementasyon yazıyorsunuz ya da başkasının paketine bağımlı kalıyorsunuz. Rust ile yazılmış bir wasm modülü ise size neredeyse native performans verirken güvenli bellek yönetimi de sağlıyor.
Gerçek dünya örneği olarak şunu ele alalım: Bir fintech uygulamasında müşteri işlemlerini client-side’da şifrelemek istiyorsunuz. JavaScript’te yazılmış bir AES implementasyonu ile Rust/wasm versiyonu arasında 3-5x performans farkı görmek son derece normal.
Ortamı Hazırlamak
Öncelikle gerekli araçları kurmanız gerekiyor. Rust zaten kuruluysa wasm-pack kurulumuna geçebilirsiniz.
# Rust kurulu değilse önce bunu yapın
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
# wasm32 target'ını ekleyin
rustup target add wasm32-unknown-unknown
# wasm-pack kurulumu
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Alternatif olarak cargo ile de kurabilirsiniz
cargo install wasm-pack
# Kurulumu doğrulayın
wasm-pack --version
rustc --version
Node.js ve npm de sisteminizde olmalı. Özellikle Node.js 16+ kullanmanızı öneririm çünkü eski versiyonlarda wasm modüllerini import ederken bazı garip davranışlar yaşanabiliyor.
İlk Projeyi Oluşturmak
wasm-pack kendi proje şablonuyla gelmiyor ama cargo-generate ile hazır bir template kullanabilirsiniz.
# cargo-generate kurulumu
cargo install cargo-generate
# wasm-pack template ile proje oluşturun
cargo generate --git https://github.com/rustwasm/wasm-pack-template
# Proje adını girmeniz istenecek, örneğin: my-wasm-lib
# Proje dizinine girin
cd my-wasm-lib
# Dosya yapısına bakın
ls -la
Oluşturulan yapı şöyle görünüyor:
- src/lib.rs: Ana Rust kaynak dosyanız
- Cargo.toml: Rust bağımlılık yönetimi
- src/utils.rs: Yardımcı fonksiyonlar (panic hook dahil)
- .gitignore: Standart ignore dosyası
Cargo.toml dosyasını inceleyelim ve ne değiştirmemiz gerektiğini görelim:
cat Cargo.toml
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2"
console_error_panic_hook = { version = "0.1", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3"
[profile.release]
opt-level = "z"
lto = true
Burada dikkat edilmesi gereken birkaç nokta var. crate-type = ["cdylib"] kısmı wasm için dinamik kütüphane oluşturulmasını sağlıyor. opt-level = "z" boyutu minimize ediyor, lto = true ise link-time optimization ile daha küçük çıktı elde etmenizi sağlıyor.
Rust Kodu Yazmak: Gerçek Bir Senaryo
Şimdi gerçekçi bir örnek yapalım. Diyelim ki bir e-ticaret platformu için ürün araması yapıyorsunuz ve fuzzy search algoritması JavaScript’te çok yavaş çalışıyor. Bunu Rust’a taşıyalım.
// src/lib.rs
use wasm_bindgen::prelude::*;
// JavaScript'te console.log() kullanabilmek için
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// Macro tanımı - geliştirme sırasında debug için kullanışlı
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
// Levenshtein distance hesaplama - fuzzy search'ün temeli
fn levenshtein_distance(s1: &str, s2: &str) -> usize {
let s1_chars: Vec<char> = s1.chars().collect();
let s2_chars: Vec<char> = s2.chars().collect();
let m = s1_chars.len();
let n = s2_chars.len();
let mut dp = vec![vec![0usize; n + 1]; m + 1];
for i in 0..=m {
dp[i][0] = i;
}
for j in 0..=n {
dp[0][j] = j;
}
for i in 1..=m {
for j in 1..=n {
if s1_chars[i - 1] == s2_chars[j - 1] {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + dp[i - 1][j - 1]
.min(dp[i - 1][j])
.min(dp[i][j - 1]);
}
}
}
dp[m][n]
}
// JavaScript'e expose edilecek ana fonksiyon
#[wasm_bindgen]
pub fn fuzzy_search(query: &str, candidates: &str, threshold: usize) -> String {
let items: Vec<&str> = candidates.split(',').collect();
let query_lower = query.to_lowercase();
let mut results: Vec<(&str, usize)> = items
.iter()
.filter_map(|&item| {
let item_lower = item.trim().to_lowercase();
let distance = levenshtein_distance(&query_lower, &item_lower);
if distance <= threshold {
Some((item.trim(), distance))
} else {
None
}
})
.collect();
// Mesafeye göre sırala (en yakın önce)
results.sort_by_key(|&(_, d)| d);
results
.iter()
.map(|&(item, _)| item)
.collect::<Vec<&str>>()
.join(",")
}
// Performans ölçümü için basit bir yardımcı
#[wasm_bindgen]
pub fn calculate_similarity_score(s1: &str, s2: &str) -> f64 {
let distance = levenshtein_distance(s1, s2);
let max_len = s1.len().max(s2.len());
if max_len == 0 {
return 1.0;
}
1.0 - (distance as f64 / max_len as f64)
}
Bu kodda #[wasm_bindgen] attribute’u JavaScript dünyasına expose etmek istediğiniz fonksiyonları işaretliyor. İçeride kalan helper fonksiyonları (levenshtein_distance gibi) JavaScript’ten görünmüyor, bu da güzel bir kapsülleme sağlıyor.
Derleme ve Paket Oluşturma
Şimdi asıl sihir burada başlıyor. wasm-pack build komutu hem derleme hem de npm paket yapısını oluşturuyor.
# Development build - debug bilgileri dahil, boyut büyük ama hata ayıklamak kolay
wasm-pack build --dev
# Release build - optimize edilmiş, production için
wasm-pack build --release
# Hedef belirtmek önemli! Farklı ortamlar için farklı build hedefleri var
# Bundler (webpack, rollup, vite için)
wasm-pack build --target bundler
# Node.js için
wasm-pack build --target nodejs
# Tarayıcı için doğrudan (bundler olmadan)
wasm-pack build --target web
# ES modülleri olmadan (eski tarayıcı desteği)
wasm-pack build --target no-modules
# Çıktı dizinini belirtin
wasm-pack build --release --target bundler --out-dir pkg
Build tamamlandığında pkg/ dizininde şunları göreceksiniz:
- my_wasm_lib_bg.wasm: Derlenmiş WebAssembly binary
- my_wasm_lib.js: JavaScript glue kodu
- my_wasm_lib.d.ts: TypeScript type tanımları
- package.json: npm için hazırlanmış manifest
Oluşturulan package.json‘a bakalım:
cat pkg/package.json
Burada name, version, ve description alanlarını düzenlemek isteyeceksiniz. Ayrıca files alanının doğru yapılandırıldığından emin olun.
TypeScript Desteği ve tip Tanımları
wasm-bindgen otomatik olarak .d.ts dosyası oluştursa da bazı durumlarda ek tip bilgisi eklemeniz gerekebilir. wasm-bindgen‘in daha zengin tip desteği için js_sys ve web_sys crate’lerini kullanabilirsiniz.
// Cargo.toml'a ekleyin
// [dependencies]
// js-sys = "0.3"
// web-sys = { version = "0.3", features = ["console"] }
use wasm_bindgen::prelude::*;
use js_sys::Array;
// JavaScript Array döndüren fonksiyon
#[wasm_bindgen]
pub fn search_and_rank(query: &str, candidates: &str) -> Array {
let items: Vec<&str> = candidates.split(',').collect();
let query_lower = query.to_lowercase();
let result = Array::new();
let mut scored: Vec<(&str, f64)> = items
.iter()
.map(|&item| {
let score = calculate_similarity_score(
&query_lower,
&item.trim().to_lowercase()
);
(item.trim(), score)
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
for (item, _score) in scored.iter().take(10) {
result.push(&JsValue::from_str(item));
}
result
}
fn calculate_similarity_score(s1: &str, s2: &str) -> f64 {
let distance = levenshtein_distance(s1, s2);
let max_len = s1.len().max(s2.len());
if max_len == 0 { return 1.0; }
1.0 - (distance as f64 / max_len as f64)
}
Test Yazmak
wasm-bindgen-test sayesinde wasm testleri hem Node.js’te hem tarayıcıda çalıştırabilirsiniz.
// src/lib.rs içine veya ayrı bir test modülüne ekleyin
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
// Testlerin tarayıcıda çalışması için bu attribute gerekli
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_exact_match() {
let result = fuzzy_search("laptop", "laptop,phone,tablet", 0);
assert_eq!(result, "laptop");
}
#[wasm_bindgen_test]
fn test_fuzzy_match() {
// "lapttop" -> "laptop" distance 1
let result = fuzzy_search("lapttop", "laptop,phone,tablet", 2);
assert!(result.contains("laptop"));
}
#[wasm_bindgen_test]
fn test_similarity_score() {
let score = calculate_similarity_score("hello", "hello");
assert!((score - 1.0).abs() < f64::EPSILON);
}
#[wasm_bindgen_test]
fn test_no_match() {
let result = fuzzy_search("xyz", "laptop,phone,tablet", 1);
assert_eq!(result, "");
}
}
Testleri çalıştırmak için:
# Node.js ile testleri çalıştır (daha hızlı)
wasm-pack test --node
# Headless Chrome ile çalıştır
wasm-pack test --headless --chrome
# Firefox ile
wasm-pack test --headless --firefox
# Belirli bir test çalıştır
wasm-pack test --node -- test_exact_match
npm’e Yayınlamak
Paket yapısı hazır, şimdi npm’e yayınlama zamanı. Önce local’de test etmek için npm link kullanabilirsiniz.
# pkg/ dizininde local link oluşturun
cd pkg
npm link
# Test projesinde linki kullanın
cd /path/to/test-project
npm link my-wasm-lib
# Test projesinde kullanım
cat > test.mjs << 'EOF'
import init, { fuzzy_search, calculate_similarity_score } from 'my-wasm-lib';
async function main() {
await init();
const products = "laptop,lapttop,phone,tablet,smartwatch,keyboard,mouse";
const results = fuzzy_search("lapto", products, 2);
console.log("Arama sonuçları:", results);
const score = calculate_similarity_score("typescript", "javascript");
console.log("Benzerlik skoru:", score.toFixed(3));
}
main();
EOF
node test.mjs
Her şey yolundaysa npm’e publish edebilirsiniz:
# npm hesabınıza giriş yapın
npm login
# Scoped paket için (önerilen)
# pkg/package.json içinde name alanını "@kullanici-adi/my-wasm-lib" yapın
# Dry-run ile kontrol edin
cd pkg
npm publish --dry-run
# Yayınlayın
npm publish --access public
CI/CD Pipeline Kurulumu
GitHub Actions ile otomatik build ve publish sürecini kuralım:
# .github/workflows/publish.yml
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Rust kurulumu
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: wasm-pack kurulumu
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Testleri çalıştır
run: wasm-pack test --node
- name: Release build
run: wasm-pack build --release --target bundler
- name: Node.js kurulumu
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: npm'e yayınla
run: npm publish --access public
working-directory: pkg
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Vite Projesiyle Entegrasyon
Modern bir frontend projesinde nasıl kullanacağınıza bakalım. Vite ile çalışmak oldukça temiz:
# Vite projesi oluşturun
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
# Wasm paketinizi ekleyin (local geliştirme için)
npm install ../my-wasm-lib/pkg
# vite.config.ts'e wasm desteği ekleyin
// src/hooks/useSearch.ts
import { useEffect, useState, useCallback } from 'react';
let wasmReady = false;
let wasmModule: typeof import('my-wasm-lib') | null = null;
async function initWasm() {
if (!wasmReady) {
wasmModule = await import('my-wasm-lib');
await wasmModule.default();
wasmReady = true;
}
return wasmModule!;
}
export function useWasmSearch(candidates: string[]) {
const [ready, setReady] = useState(false);
useEffect(() => {
initWasm().then(() => setReady(true));
}, []);
const search = useCallback(async (query: string, threshold = 2) => {
if (!ready || !wasmModule) return [];
const candidateStr = candidates.join(',');
const resultStr = wasmModule.fuzzy_search(query, candidateStr, threshold);
return resultStr ? resultStr.split(',') : [];
}, [ready, candidates]);
return { search, ready };
}
Paket Boyutunu Optimize Etmek
Production’da wasm dosya boyutu önemli. Birkaç pratik öneri:
# wasm-opt aracını kullanın (binaryen paketinden gelir)
# wasm-pack genellikle bunu otomatik yapar ama manuel de çalıştırabilirsiniz
cargo install wasm-opt
# Boyutu kontrol edin
ls -lh pkg/*.wasm
# Cargo.toml'da agresif optimizasyon
# [profile.release]
# opt-level = "z" # Boyut için optimize et
# lto = true # Link-time optimization
# codegen-units = 1 # Daha iyi optimizasyon için tekli kod üretimi
# panic = "abort" # Panic handler boyutunu azaltır
# Dead code elimination için
wasm-pack build --release -- --features wee_alloc
wee_alloc crate’i, varsayılan Rust allocator yerine çok daha küçük bir allocator kullanmanızı sağlıyor. Özellikle küçük utility paketleri için 10-15kb fark yaratabilir.
# Cargo.toml
[dependencies]
wee_alloc = { version = "0.4", optional = true }
[features]
default = ["wee_alloc"]
// src/lib.rs başına ekleyin
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
Yaygın Sorunlar ve Çözümleri
Geliştirme sırasında karşılaşacağınız bazı durumlar ve çözümleri:
CORS hatası alıyorsanız: Wasm dosyaları application/wasm MIME tipiyle servis edilmeli. Nginx’te bunu şöyle ekleyebilirsiniz:
# /etc/nginx/mime.types dosyasına ekleyin
# application/wasm wasm;
# Veya site konfigürasyonuna
# add_header 'Content-Type' 'application/wasm' for .wasm files
Büyük bağımlılıklar wasm boyutunu şişiriyorsa: cargo-bloat ile analiz yapın:
cargo install cargo-bloat
cargo bloat --release --crates --target wasm32-unknown-unknown
JavaScript ile tip uyumsuzluğu varsa: wasm-bindgen‘in serde desteğini kullanabilirsiniz:
# Cargo.toml'a ekleyin
# serde = { version = "1", features = ["derive"] }
# serde-wasm-bindgen = "0.6"
Sonuç
wasm-pack ile Rust kodunu npm paketine dönüştürmek, ilk bakışta karmaşık görünse de adımları takip ettiğinizde oldukça akıcı bir süreç. Özetleyecek olursak: Rust kodu yazıyorsunuz, wasm-bindgen attribute’larıyla JavaScript’e ne expose edeceğinizi belirliyorsunuz, wasm-pack build ile paketi oluşturuyorsunuz ve standart npm workflow’uyla yayınlıyorsunuz.
Bu yaklaşımın en büyük avantajı, Rust’ın sunduğu tip güvenliği ve performansı JavaScript ekosistemiyle birleştirmeniz. TypeScript tanımları otomatik oluşturuluyor, test altyapısı hazır geliyor ve modern bundler’larla entegrasyon sorunsuz çalışıyor.
Eğer projelerinizde hesap yoğun işlemler, kriptografi, veri sıkıştırma veya karmaşık algoritma gereksinimleri varsa, bu kombinasyonu denemenizi kesinlikle tavsiye ederim. İlk kurulum maliyeti biraz yüksek ama uzun vadede hem performans kazancı hem de Rust’ın sağladığı güvenlik ciddi avantajlar sunuyor.
