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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir