C ve C++ Kodunu WebAssembly’ye Derleme: Emscripten Rehberi
Yıllarca C ve C++ ile yazılmış kritik kütüphaneleri, hesaplama yoğun algoritmalar ve battle-tested kodları düşünün. Bunları bir anda çöpe atmak yerine, doğrudan tarayıcıda çalıştırabilseydiniz ne olurdu? İşte Emscripten tam olarak bunu yapmanızı sağlıyor. WebAssembly’nin yükselişiyle birlikte, C/C++ ekosisteminin onlarca yıllık birikimi artık web platformuna taşınabiliyor. Bu yazıda Emscripten toolchain’ini sıfırdan kuracak, gerçek dünya senaryolarıyla C ve C++ kodlarını WASM’a derleyeceğiz.
Emscripten Nedir ve Neden Önemlidir
Emscripten, LLVM tabanlı bir derleyici toolchain’idir. Temel işlevi C/C++ kaynak kodunu önce LLVM IR’a (Intermediate Representation), ardından WebAssembly bytecode’una dönüştürmektir. Arka planda Clang kullanır ve çıktı olarak .wasm dosyasının yanı sıra JavaScript glue kodu da üretir.
Peki neden bu kadar önemli? Düşünün; OpenSSL’in bir kısmını, FFmpeg codec’lerini, fizik motorlarını (Bullet Physics gibi) veya görüntü işleme kütüphanelerini sıfırdan JavaScript ile yazmak zorunda değilsiniz. Emscripten bu köprüyü kurduğu için günümüzde Figma, AutoCAD Web ve Google Earth gibi büyük uygulamalar WebAssembly altyapısı üzerinde koşuyor.
Kurulum ve Ortam Hazırlığı
Emscripten SDK’sı (emsdk) kurulumu oldukça basit ama bazı bağımlılıklar gerekiyor.
Linux/macOS için önkoşullar:
- Python 3.6 veya üzeri
- Git
- CMake (opsiyonel ama çoğu projede gerekli)
- Node.js (test için)
# Ubuntu/Debian sistemlerde bağımlılıkları kur
sudo apt update
sudo apt install -y python3 python3-pip git cmake nodejs npm
# emsdk reposunu klonla
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# En güncel Emscripten sürümünü indir ve aktifleştir
./emsdk install latest
./emsdk activate latest
# Ortam değişkenlerini yükle (her terminal oturumunda çalıştır)
source ./emsdk_env.sh
Kalıcı hale getirmek için ~/.bashrc veya ~/.zshrc dosyanıza şu satırı ekleyin:
# emsdk_env.sh yolunu kendi kurulumunuza göre düzenleyin
echo 'source ~/emsdk/emsdk_env.sh' >> ~/.bashrc
source ~/.bashrc
# Kurulumu doğrula
emcc --version
# Çıktı: emcc (Emscripten gcc/clang-like replacement) 3.x.x
Windows üzerinde kurulum için Git Bash veya WSL2 kullanmanızı şiddetle tavsiye ederim. Native Windows’ta da çalışıyor ama Linux/WSL2 üzerindeki deneyim çok daha pürüzsüz.
İlk Derleme: Merhaba Dünya
Teorik kısımları bir kenara bırakıp hemen pratiğe geçelim. Klasik C programımızı WASM’a derleyelim.
// hello.c
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Merhaba WASM Dunyasi!n");
// Basit bir hesaplama
int toplam = 0;
for (int i = 1; i <= 100; i++) {
toplam += i;
}
printf("1'den 100'e kadar toplam: %dn", toplam);
return 0;
}
# Temel derleme komutu
emcc hello.c -o hello.html
# Bu komut üç dosya üretir:
# hello.wasm -> WebAssembly bytecode
# hello.js -> JavaScript glue kodu
# hello.html -> Test için hazır HTML sayfası
# Node.js ile test et
node hello.js
# Tarayıcıda test için basit HTTP sunucusu kur
python3 -m http.server 8080
# Tarayıcıda http://localhost:8080/hello.html adresine git
Dikkat edin, WASM dosyalarını doğrudan file:// protokolüyle açamazsınız. Tarayıcılar güvenlik politikaları gereği bir HTTP sunucusu gerektiriyor.
JavaScript’ten C Fonksiyonlarını Çağırma
Gerçek dünya senaryolarında genellikle tüm programı değil, belirli C/C++ fonksiyonlarını JavaScript tarafından çağırmak istersiniz. Bunun için EXPORTED_FUNCTIONS ve cwrap mekanizması devreye giriyor.
// matematik.c
#include <emscripten.h>
#include <math.h>
// EMSCRIPTEN_KEEPALIVE direktifi olmadan linker bu fonksiyonu
// kullanılmadığı için dışarıda bırakabilir
EMSCRIPTEN_KEEPALIVE
double faktoriyel(int n) {
if (n <= 1) return 1.0;
double sonuc = 1.0;
for (int i = 2; i <= n; i++) {
sonuc *= i;
}
return sonuc;
}
EMSCRIPTEN_KEEPALIVE
double kare_kok(double x) {
return sqrt(x);
}
EMSCRIPTEN_KEEPALIVE
int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1, gecici;
for (int i = 2; i <= n; i++) {
gecici = a + b;
a = b;
b = gecici;
}
return b;
}
# Fonksiyonları export ederek derle
emcc matematik.c -o matematik.js
-s EXPORTED_FUNCTIONS='["_faktoriyel","_kare_kok","_fibonacci"]'
-s EXPORTED_RUNTIME_METHODS='["cwrap","ccall"]'
-s MODULARIZE=1
-s EXPORT_NAME='MatematikModule'
-O2
-lm
# Parametreler:
# -s EXPORTED_FUNCTIONS: Dışarıya açılacak C fonksiyonları (önünde _ var!)
# -s EXPORTED_RUNTIME_METHODS: JS tarafında kullanılacak yardımcı metodlar
# -s MODULARIZE=1: Tek bir module objesi olarak paketler
# -s EXPORT_NAME: Modülün global adı
# -O2: Optimizasyon seviyesi (0-3 arası)
# -lm: Math kütüphanesini linkle
JavaScript tarafında bu modülü şöyle kullanırsınız:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>WASM Matematik Kütüphanesi</title>
</head>
<body>
<script src="matematik.js"></script>
<script>
MatematikModule().then(function(Module) {
// cwrap ile fonksiyonları JavaScript'e bağla
const faktoriyel = Module.cwrap('faktoriyel', 'number', ['number']);
const kareKok = Module.cwrap('kare_kok', 'number', ['number']);
const fibonacci = Module.cwrap('fibonacci', 'number', ['number']);
console.log('10! =', faktoriyel(10)); // 3628800
console.log('√144 =', kareKok(144)); // 12
console.log('Fib(20) =', fibonacci(20)); // 6765
// ccall ile tek seferlik çağrı
const sonuc = Module.ccall(
'faktoriyel', // Fonksiyon adı
'number', // Dönüş tipi
['number'], // Parametre tipleri
[15] // Parametre değerleri
);
console.log('15! =', sonuc);
});
</script>
</body>
</html>
Gerçek Dünya Senaryosu: Görüntü İşleme
Pratik bir örnek yapalım. Gri tonlama dönüşümü yapan bir C fonksiyonu yazalım ve bunu tarayıcıda Canvas API ile entegre edelim. Bu tür pixel işlemleri JavaScript’ten 3-5 kat daha hızlı çalışır.
// goruntu.c
#include <emscripten.h>
#include <stdlib.h>
#include <string.h>
// RGBA formatındaki görüntüyü gri tonlamaya çevir
EMSCRIPTEN_KEEPALIVE
void gri_tonlama(unsigned char* piksel_verisi, int genislik, int yukseklik) {
int toplam_piksel = genislik * yukseklik;
for (int i = 0; i < toplam_piksel; i++) {
int offset = i * 4; // Her piksel 4 byte (R, G, B, A)
unsigned char r = piksel_verisi[offset];
unsigned char g = piksel_verisi[offset + 1];
unsigned char b = piksel_verisi[offset + 2];
// Luminance formülü (ITU-R BT.709)
unsigned char gri = (unsigned char)(0.2126 * r + 0.7152 * g + 0.0722 * b);
piksel_verisi[offset] = gri;
piksel_verisi[offset + 1] = gri;
piksel_verisi[offset + 2] = gri;
// Alpha kanalı değişmez: piksel_verisi[offset + 3]
}
}
// Gaussian blur (basitleştirilmiş 3x3 kernel)
EMSCRIPTEN_KEEPALIVE
void bulanik_yap(unsigned char* girdi, unsigned char* cikti,
int genislik, int yukseklik) {
// Kenar piksellerini kopyala
memcpy(cikti, girdi, genislik * yukseklik * 4);
for (int y = 1; y < yukseklik - 1; y++) {
for (int x = 1; x < genislik - 1; x++) {
for (int kanal = 0; kanal < 3; kanal++) {
int toplam = 0;
// 3x3 komşuluk ortalaması
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int idx = ((y + dy) * genislik + (x + dx)) * 4 + kanal;
toplam += girdi[idx];
}
}
int cikti_idx = (y * genislik + x) * 4 + kanal;
cikti[cikti_idx] = (unsigned char)(toplam / 9);
}
}
}
}
// Bellek yönetimi için yardımcı fonksiyonlar
EMSCRIPTEN_KEEPALIVE
unsigned char* tampon_olustur(int boyut) {
return (unsigned char*)malloc(boyut);
}
EMSCRIPTEN_KEEPALIVE
void tampon_sil(unsigned char* ptr) {
free(ptr);
}
# Görüntü işleme modülünü derle
emcc goruntu.c -o goruntu.js
-s EXPORTED_FUNCTIONS='["_gri_tonlama","_bulanik_yap","_tampon_olustur","_tampon_sil"]'
-s EXPORTED_RUNTIME_METHODS='["cwrap","HEAPU8"]'
-s ALLOW_MEMORY_GROWTH=1
-s MODULARIZE=1
-s EXPORT_NAME='GoruntuModule'
-O3
JavaScript tarafında Canvas ile entegrasyon:
// goruntu_isle.js
GoruntuModule().then(function(Module) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Görüntüyü canvas'a yükle
const img = new Image();
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const pikselSayisi = img.width * img.height * 4;
// WASM belleğine kopyala
const wasmTampon = Module._tampon_olustur(pikselSayisi);
// Module.HEAPU8 -> WASM'ın bellek görünümü
Module.HEAPU8.set(imageData.data, wasmTampon);
// Gri tonlama uygula
Module._gri_tonlama(wasmTampon, img.width, img.height);
// Sonucu geri oku
const sonucVeri = Module.HEAPU8.subarray(wasmTampon, wasmTampon + pikselSayisi);
imageData.data.set(sonucVeri);
ctx.putImageData(imageData, 0, 0);
// Belleği temizle
Module._tampon_sil(wasmTampon);
};
img.src = 'test.jpg';
});
C++ ve Sınıfları WASM’a Taşıma
C++ sınıflarını JavaScript’e açmak için Emscripten’in Embind sistemi kullanılır. Bu, çok daha temiz bir API yüzeyi sağlar.
// vektor_matematik.cpp
#include <emscripten/bind.h>
#include <cmath>
#include <string>
class Vektor3D {
public:
float x, y, z;
Vektor3D(float x, float y, float z) : x(x), y(y), z(z) {}
float uzunluk() const {
return std::sqrt(x*x + y*y + z*z);
}
Vektor3D topla(const Vektor3D& diger) const {
return Vektor3D(x + diger.x, y + diger.y, z + diger.z);
}
float nokta_carpim(const Vektor3D& diger) const {
return x * diger.x + y * diger.y + z * diger.z;
}
Vektor3D normalize() const {
float uzun = uzunluk();
if (uzun < 0.0001f) return Vektor3D(0, 0, 0);
return Vektor3D(x/uzun, y/uzun, z/uzun);
}
std::string to_string() const {
return "(" + std::to_string(x) + ", " +
std::to_string(y) + ", " +
std::to_string(z) + ")";
}
};
// Embind ile JavaScript'e bağla
EMSCRIPTEN_BINDINGS(vektor_modulu) {
emscripten::class_<Vektor3D>("Vektor3D")
.constructor<float, float, float>()
.property("x", &Vektor3D::x)
.property("y", &Vektor3D::y)
.property("z", &Vektor3D::z)
.function("uzunluk", &Vektor3D::uzunluk)
.function("topla", &Vektor3D::topla)
.function("noktaCarpim", &Vektor3D::nokta_carpim)
.function("normalize", &Vektor3D::normalize)
.function("toString", &Vektor3D::to_string);
}
# C++ Embind ile derleme
em++ vektor_matematik.cpp -o vektor.js
-lembind
-s MODULARIZE=1
-s EXPORT_NAME='VektorModule'
-O2
# JavaScript'te kullanım:
# const module = await VektorModule();
# const v1 = new module.Vektor3D(1, 2, 3);
# const v2 = new module.Vektor3D(4, 5, 6);
# console.log(v1.uzunluk());
# const v3 = v1.topla(v2);
# v1.delete(); v2.delete(); v3.delete(); // Bellek yönetimi!
Optimizasyon ve Debug Teknikleri
Performans ve hata ayıklama konusunda birkaç kritik nokta var.
# Debug build - hata ayıklama bilgileriyle
emcc kaynak.c -o debug.html
-g4
-s ASSERTIONS=2
-s SAFE_HEAP=1
-s STACK_OVERFLOW_CHECK=2
-gsource-map
# Production build - maksimum optimizasyon
emcc kaynak.c -o prod.js
-O3
-s WASM=1
-s ELIMINATE_DUPLICATE_FUNCTIONS=1
-flto
--closure 1
# Boyut optimizasyonu (küçük WASM dosyası için)
emcc kaynak.c -o kucuk.js
-Oz
-s MINIMAL_RUNTIME=1
-s ENVIRONMENT='web'
--no-entry
# WASM dosya boyutunu kontrol et
wasm-opt -Oz -o optimize.wasm ham.wasm # binaryen araçlarıyla
ls -lh *.wasm
Önemli optimizasyon parametreleri:
- -O0: Optimizasyon yok, en hızlı derleme, debug için
- -O1: Temel optimizasyonlar
- -O2: Dengeli, production için genellikle yeterli
- -O3: Agresif optimizasyon, derleme süresi uzar
- -Oz: Boyut odaklı optimizasyon, hız ikinci planda
- -s ASSERTIONS=0: Runtime kontrolleri kapatır, hız artışı sağlar
- -s ALLOW_MEMORY_GROWTH=1: Dinamik bellek büyümesine izin verir
- –closure 1: Google Closure Compiler ile JS minification
CMake ile Büyük Projeleri Derleme
Gerçek projelerde genellikle tek bir C dosyası değil, onlarca dosyadan oluşan yapılar söz konusudur. CMake entegrasyonu burada devreye girer.
# Mevcut CMake projesini WASM için yapılandır
mkdir build-wasm && cd build-wasm
# emcmake, cmake'yi Emscripten toolchain'iyle sarmalar
emcmake cmake ..
-DCMAKE_BUILD_TYPE=Release
-DBUILD_SHARED_LIBS=OFF
# Derlemeyi başlat
emmake make -j$(nproc)
# Veya ninja kullanıyorsanız
emcmake cmake .. -G Ninja
emmake ninja
Projenizin CMakeLists.txt dosyasına WASM’a özgü ayarları eklemek için:
# CMakeLists.txt içine eklenecek WASM konfigürasyonu
if(EMSCRIPTEN)
set_target_properties(hedef_ad PROPERTIES
SUFFIX ".js"
LINK_FLAGS "-s MODULARIZE=1
-s EXPORT_NAME='ProjeMod'
-s EXPORTED_FUNCTIONS=['_main']
-s ALLOW_MEMORY_GROWTH=1
-s WASM=1
-O2"
)
endif()
Dosya Sistemi ve Async İşlemler
WASM ortamında dosya sistemi erişimi farklı çalışır. Emscripten sanal bir dosya sistemi (FS) sunar.
# Statik dosyaları WASM'a göm
emcc program.c -o cikti.js
--embed-file veri_dosyasi.dat
--embed-file ayarlar/
-s FORCE_FILESYSTEM=1
# Veya runtime'da yüklenecek dosyalar için
emcc program.c -o cikti.js
--preload-file varliklar/
-s FORCE_FILESYSTEM=1
// async_ornegi.c - setTimeout ve fetch benzeri async işlemler
#include <emscripten.h>
#include <stdio.h>
// JavaScript'ten çağrılacak callback örüntüsü
void islem_tamamlandi(void* kullanici_verisi, void* tampon, int boyut) {
printf("Veri alindi, boyut: %d bytesn", boyut);
// İşleme devam et...
}
void islem_basarisiz(void* kullanici_verisi) {
printf("Islem basarisiz oldu!n");
}
int main() {
// emscripten_async_wget2_data ile async HTTP isteği
emscripten_async_wget2_data(
"https://api.example.com/veri", // URL
"GET", // Method
"", // Post verisi
NULL, // Kullanıcı verisi
1, // Free sonrası
islem_tamamlandi,
islem_basarisiz,
NULL
);
// Ana döngüyü async beklet
emscripten_set_main_loop(NULL, 0, 1);
return 0;
}
Yaygın Sorunlar ve Çözümleri
Emscripten ile çalışırken sık karşılaşılan sorunları ve çözümlerini bilmek kritik.
Linking hataları: _fonksiyon_adi bulunamıyor hatası aldığınızda, EXPORTED_FUNCTIONS listesini kontrol edin. C++ fonksiyonları için name mangling söz konusu, extern "C" kullanın.
Bellek hataları: Stack overflow veya heap corruption için -s ASSERTIONS=2 -s SAFE_HEAP=1 ile debug build alın. C++ nesnelerini kullandıktan sonra mutlaka .delete() çağırın, yoksa bellek sızıntısı olur.
CORS hataları: .wasm dosyaları için sunucu Content-Type: application/wasm header’ı dönmeli. Nginx için /etc/nginx/mime.types dosyasına application/wasm wasm; ekleyin.
Yavaş derleme: Incremental build için -MMD -MP flag’lerini ve ccache kullanabilirsiniz.
# ccache ile hızlı derleme
sudo apt install ccache
export EM_COMPILER_WRAPPER=ccache
emcc kaynak.c -o cikti.js -O2
# Wasm dosyasını analiz et (ne kadar yer kaplıyor?)
wasm2wat cikti.wasm | head -50 # WAT formatında incele
Sonuç
Emscripten ve WebAssembly ikilisi, C/C++ ekosisteminin web platformuyla buluşmasını sağlayan güçlü bir köprüdür. Başlangıçta toolchain kurulumu ve derleme parametreleri karmaşık görünse de, bir kez alışkınlık kazandığınızda iş akışı oldukça verimli hale geliyor.
Özetlersek; temel emcc komutuyla başlayın, EMSCRIPTEN_KEEPALIVE ve cwrap ile fonksiyon bağlamayı öğrenin, C++ projeleri için Embind‘e geçin ve büyük projelerde emcmake/emmake kombinasyonunu kullanın. Optimizasyon için production’da -O2 veya -O3 yeterli olurken, boyut kritikse -Oz tercih edin.
En önemli pratik tavsiye: Her zaman önce debug build alın (-g4 -s ASSERTIONS=2), her şey çalışır hale gelince production optimizasyonlarını ekleyin. WASM’ın getirdiği performans kazanımları, özellikle hesaplama yoğun işlemlerde JavaScript’e kıyasla 2x-10x arasında olabiliyor. Bu fark, görüntü işleme, ses analizi, kriptografi ve fizik simülasyonu gibi alanlarda gerçekten hissedilir düzeyde.
