
React Native JSI Modülü Geliştirme: C++ Host Object Pattern ile Sıfırdan Mimari İnşası

Giriş
React Native ekosisteminde yıllarca uygulamalarımızın performans darboğazı tek bir noktada düğümlendi: Asenkron Bridge. JavaScript ile Native ortam arasındaki her iletişim, verinin JSON formatında serileştirilmesi, bir kuyruğa alınması (batching) ve karşı tarafta tekrar deserileştirilmesi süreçlerinden geçiyordu. Saniyede 60 kare (60 FPS) hedefleyen bir animasyonda veya 120Hz tarama hızına sahip bir ivmeölçer verisini işlerken, bu köprü üzerinde yaşanan 2-3 milisaniyelik ek yük (overhead), doğrudan frame düşüşlerine (jank) ve UI thread bloklanmalarına neden oluyordu.
JSI (JavaScript Interface), bu mimariyi kökünden değiştiriyor. JS motoru (Hermes veya V8) ile C++ belleği arasında doğrudan, senkron bir referans köprüsü kurarak aradaki serileştirme maliyetini sıfıra indiriyor. Geleneksel mimaride 3.000 mikrosaniye süren bir metot çağrısı, JSI üzerinden yapıldığında ortalama 10-15 mikrosaniye bandına iniyor. Bu oran, CPU döngülerinden muazzam bir tasarruf ve gerçek zamanlı veri işleme kapasitesi anlamına gelir.
Bu makalede, React Native uygulamanızda doğrudan bellek erişimi sunan, JavaScript çöp toplayıcısı (Garbage Collector) ile senkronize çalışan bir C++ HostObject modülünün üretim ortamı (production) standartlarında sıfırdan nasıl inşa edileceğini adım adım inceleyeceğiz.
İçindekiler
- Adım 1: C++ Host Object Sınıfının Tasarlanması
- Adım 2: JSI Fonksiyonlarının ve Özelliklerinin Eklenmesi
- Adım 3: iOS Entegrasyonu (Objective-C++ Bağlantısı)
- Adım 4: Android Entegrasyonu (JNI ve Bellek Adresi İletimi)
- Adım 5: JavaScript Katmanında Tüketim ve Tip Güvenliği
- Trade-off Analizi: JSI vs Eski Bridge vs TurboModules
- Pratik Öneriler / Production Notları
- Sık Sorulan Sorular
- Sonuç
Adım 1: C++ Host Object Sınıfının Tasarlanması
JSI mimarisinin kalbinde facebook::jsi::HostObject sınıfı yatar. Bu sınıf, JavaScript dünyasına maruz bırakacağımız (expose edeceğimiz) C++ nesnelerinin temelini oluşturur. JavaScript motoru, bu nesnenin yaşam döngüsünü std::shared_ptr üzerinden yönetir; yani JS tarafında nesneye olan referans koptuğunda, C++ tarafındaki bellek sızıntısız bir şekilde temizlenir.
İlk olarak, karmaşık matematiksel hesaplamaları senkronize olarak yapacak MathEngine isimli bir sınıf tasarlayalım.
#pragma once
#include <jsi/jsi.h>
#include <vector>
using namespace facebook::jsi;
class MathEngine : public HostObject {
public:
MathEngine();
~MathEngine();
// JS tarafından bir özelliğe (property veya fonksiyon) erişildiğinde tetiklenir
Value get(Runtime& rt, const PropNameID& name) override;
// JS tarafından bir özelliğe değer atandığında tetiklenir
void set(Runtime& rt, const PropNameID& name, const Value& value) override;
// C++ dünyasında belleği doğrudan kullanan sınıfa özel bir metot
std::vector<jsi::PropNameID> getPropertyNames(Runtime& rt) override;
private:
int callCount = 0;
};
Burada get metodu, JS katmanından mathEngine.multiply çağrıldığında devreye giren bir nevi yönlendiricidir (router). JS motoruna hangi değerin veya fonksiyonun döndürüleceğine bu aşamada karar veririz.
Adım 2: JSI Fonksiyonlarının ve Özelliklerinin Eklenmesi
Başlık dosyamızı (Header) oluşturduktan sonra, C++ implementasyonuna geçiyoruz. Bu aşamada JavaScript tarafına hem statik bir özellik (property) hem de senkron çalışan bir fonksiyon (method) sunacağız. JS’den gelen argümanların tip kontrollerini atlamamak, production ortamındaki kilit noktalardan biridir.
#include "MathEngine.h"
#include <stdexcept>
MathEngine::MathEngine() {}
MathEngine::~MathEngine() {}
Value MathEngine::get(Runtime& rt, const PropNameID& name) {
auto propName = name.utf8(rt);
// 1. Statik Değer Döndürme
if (propName == "callCount") {
return Value(callCount);
}
// 2. Fonksiyon Döndürme (Senkron İletişim)
if (propName == "multiply") {
return Function::createFromHostFunction(
rt,
PropNameID::forAscii(rt, "multiply"),
2, // Beklenen argüman sayısı
[this](Runtime& runtime, const Value& thisValue, const Value* arguments, size_t count) -> Value {
// Tip kontrolü (Production standartı)
if (count < 2 || !arguments[0].isNumber() || !arguments[1].isNumber()) {
throw JSError(runtime, "'multiply' fonksiyonu iki adet sayısal argüman gerektirir.");
}
double a = arguments[0].asNumber();
double b = arguments[1].asNumber();
this->callCount++; // Durum (state) yönetimi
return Value(a * b);
}
);
}
// Bulunamayan özellikler için undefined dön
return Value::undefined();
}
void MathEngine::set(Runtime& rt, const PropNameID& name, const Value& value) {
auto propName = name.utf8(rt);
if (propName == "callCount") {
throw JSError(rt, "'callCount' özelliği salt okunurdur (read-only).");
}
}
std::vector<PropNameID> MathEngine::getPropertyNames(Runtime& rt) {
std::vector<PropNameID> names;
names.push_back(PropNameID::forAscii(rt, "callCount"));
names.push_back(PropNameID::forAscii(rt, "multiply"));
return names;
}
Yukarıdaki implementasyonda Function::createFromHostFunction ile C++ Lambda fonksiyonunu bir JavaScript nesnesine çeviriyoruz. JSError fırlatmamızın nedeni, standart std::invalid_argument kullanımının doğrudan uygulama çökmesine (SIGABRT) sebep olmasıdır. JSError, hatayı JavaScript katmanında bir TypeError veya Error olarak yakalanabilir hale getirir.
Adım 3: iOS Entegrasyonu (Objective-C++ Bağlantısı)
C++ sınıfımız hazır. Şimdi, React Native’in iOS platformunda JS çalışma zamanı (Runtime) belleğine erişip bu sınıfı enjekte etmemiz gerekiyor. Bunun için standart bir React Bridge modülü oluşturup, .mm (Objective-C++) uzantısıyla JSI nesnemizi global objeye bağlayacağız.
// MathInstaller.mm
#import <React/RCTBridge+Private.h>
#import <React/RCTUtils.h>
#import <jsi/jsi.h>
#import "MathEngine.h"
#import "MathInstaller.h"
@implementation MathInstaller
RCT_EXPORT_MODULE()
// JSI Binding işlemleri ana thread'i engellememeli, ancak belleğe yazma işlemi eşzamanlı olmalı.
RCT_EXPORT_METHOD(install)
{
RCTBridge *bridge = [RCTBridge currentBridge];
RCTCxxBridge *cxxBridge = (RCTCxxBridge *)bridge;
if (cxxBridge.runtime == nullptr) {
return;
}
facebook::jsi::Runtime &jsiRuntime = *(facebook::jsi::Runtime *)cxxBridge.runtime;
// C++ Host Object'i std::shared_ptr olarak oluşturuyoruz
auto mathEngine = std::make_shared<MathEngine>();
// JS Tarafında global.mathEngine = MathEngine objesi atamasını yapıyoruz
jsiRuntime.global().setProperty(
jsiRuntime,
"mathEngine",
facebook::jsi::Object::createFromHostObject(jsiRuntime, mathEngine)
);
}
@end
install metodunu JavaScript tarafından uygulamanın başlangıcında bir kez çağırdıktan sonra, mathEngine nesnesi JS Garbage Collector’a teslim edilir ve global bağlamda (window/global) erişilebilir hale gelir.
Adım 4: Android Entegrasyonu (JNI ve Bellek Adresi İletimi)
Android’de JSI entegrasyonu, iOS’a göre daha fazla efor gerektirir çünkü Java ve C++ (JNI) katmanları arasında JS Motoru’nun bellek adresini (pointer) taşımamız gerekir.
Java Modülünde (MathModule.java):
package com.jsiapp;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class MathModule extends ReactContextBaseJavaModule {
static {
System.loadLibrary("mathengine");
}
public MathModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@NonNull
@Override
public String getName() {
return "MathModule";
}
@ReactMethod(isBlockingSynchronousMethod = true)
public void install() {
// JS Runtime'ın bellekteki hafıza adresini (pointer) C++ tarafına gönderiyoruz
long jsContextPointer = getReactApplicationContext().getJavaScriptContextHolder().get();
nativeInstall(jsContextPointer);
}
private native void nativeInstall(long jsiPtr);
}
C++ JNI Katmanı (cpp-adapter.cpp):
#include <jni.h>
#include <jsi/jsi.h>
#include "MathEngine.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_jsiapp_MathModule_nativeInstall(JNIEnv *env, jobject thiz, jlong jsiPtr) {
// Pointer'ı jsi::Runtime nesnesine cast (dönüştürme) ediyoruz
facebook::jsi::Runtime* runtime = reinterpret_cast<facebook::jsi::Runtime*>(jsiPtr);
if (runtime) {
auto mathEngine = std::make_shared<MathEngine>();
runtime->global().setProperty(
*runtime,
"mathEngine",
facebook::jsi::Object::createFromHostObject(*runtime, mathEngine)
);
}
}
Bu bağlantı, Java katmanından bağımsız olarak JS ve C++’ın Android üzerinde doğrudan iletişim kurmasını sağlayan kapıyı aralar.
Adım 5: JavaScript Katmanında Tüketim ve Tip Güvenliği
C++ kodunun JavaScript’e tip güvenliği sağlamadan (TypeScript olmadan) aktarılması, production ortamında kritik çalışma zamanı hatalarına yol açar. Global değişkene yazdığımız objenin TypeScript tanımını (Declaration) yapmalıyız.
// MathEngine.ts
import { NativeModules } from 'react-native';
interface MathEngine {
readonly callCount: number;
multiply(a: number, b: number): number;
}
// Global objenin tipini genişletiyoruz
declare global {
var mathEngine: MathEngine | undefined;
}
// Eğer uygulama yeni başlatıldıysa ve bellek henüz yüklenmediyse native modülü tetikle
if (global.mathEngine == null) {
const MathInstaller = NativeModules.MathInstaller ?? NativeModules.MathModule;
if (MathInstaller && MathInstaller.install) {
MathInstaller.install();
} else {
console.error("MathEngine JSI modülü yüklenemedi. Native linkleme hatası olabilir.");
}
}
// Artık global bağlamdan senkron ve güvenli şekilde çağrılabilir
export const getCallCount = () => global.mathEngine?.callCount ?? 0;
export const multiply = (a: number, b: number) => {
if (!global.mathEngine) return 0;
return global.mathEngine.multiply(a, b);
};
Trade-off Analizi: JSI vs Eski Bridge vs TurboModules
| Mimari/Metot | İletişim Türü | Gecikme (Overhead) | Kullanım Senaryosu ve Karar Ağacı (Trade-off) |
|---|---|---|---|
| Eski Bridge | Asenkron (JSON) | 2 – 5 ms | Uzun süren disk işlemleri, arka plan indirmeleri ve UI’ı dondurmaması gereken ağır işlemler. Senkron olma zorunluluğu yoksa en risksiz metottur. |
| Raw JSI (Bu Makale) | Senkron (Doğrudan) | ~15 mikrosaniye | Yüksek frekanslı sensör okuma, SQLite sorguları, real-time ses işleme. Dezavantaj: C++ thread’inde işlem 50ms sürerse, JS thread 50ms kilitlenir (Frame Drop). |
| TurboModules | Lazy Senkron/Asenkron | ~30-50 mikrosaniye | Tip güvenliği sağlayan (Codegen destekli) standartlaşmış yeni mimari. Raw JSI kadar esnek olmasa da, mimari standartları dayattığı için kurumsal projelerde tercih edilmelidir. |
Eğer uygulamanızda çalıştıracağınız C++ fonksiyonu yoğun bir G/Ç işlemi (I/O) yapıyor veya 16.6ms’lik (60 FPS bütçesi) süreyi aşıyorsa, JSI üzerinden fonksiyon çağrısını senkronize bekletmek büyük bir mimari hatadır. Böyle bir senaryoda, C++ tarafında bir worker thread oluşturmalı ve JSI üzerinden JavaScript’e bir Promise döndürerek (jsi::Function ile) asenkron çözümü kendiniz inşa etmelisiniz.
Pratik Öneriler / Production Notları
JSI ile C++ kodu yazarken React Native’in varsayılan güvenlik ağının dışında çıkarsınız. Üretim ortamında uygulamanızın çökmelerden (crash) korunması için şu kontrol listesini uygulayın:
- Thread Safety (İş Parçacığı Güvenliği):
jsi::Runtimenesnesi thread-safe değildir. C++ arka plan iş parçacığından (background thread)rtreferansına erişmek anında SIGSEGV (Segmentation Fault) hatasına neden olur. Asenkron işlemlerden sonra JS çağırmak için React Native’inCallInvokeryapısını kullanın. - Memory Leaks (Bellek Sızıntıları):
HostObjecther zamanstd::shared_ptrveyastd::unique_ptrile yönetilmelidir. Native işaretçileri (raw pointer)deleteile manuel silmeye çalışmayın; JS çöp toplayıcısına bırakın. - Exception Handling: C++ tarafında fırlatılan standart
std::exceptiontürevleri, JS’e taşınamaz. Tüm kritik kod bloklarınıtry/catchiçine alıp, hatalarıjsi::JSErrorolarak yeniden fırlatmalısınız (Adım 2’deki gibi). - NDK Sürüm Senkronizasyonu: Android cihazlarda oluşan JNI çökmelerinin yüzde 80’i uyumsuz NDK derlemelerinden kaynaklanır.
android/build.gradledosyanızdandkVersiontanımının React Native’in talep ettiği (örneğin 0.72 için 23.1.7779620) sürümle birebir eşleştiğinden emin olun.
Sık Sorulan Sorular
Raw JSI ile TurboModules arasındaki fark nedir?
TurboModules, JSI altyapısını kullanan ancak React Native ekibi tarafından standartlaştırılmış, C++ boilerplate kodunu otomatik üreten (Codegen) sistemdir. Raw JSI ise bu makalede yaptığımız gibi HostObject‘i manuel implemente etme sürecidir. Raw JSI, TurboModule mimarisinin dayattığı kısıtlamalara takılmadan özel bellek yönetimi yapmanız gerektiğinde gereklidir.
JSI Modülüm sadece Hermes ile mi çalışır?
Hayır. JSI bir soyutlama katmanıdır (abstraction layer). Yazdığınız C++ kodu Hermes, JavaScriptCore (JSC) veya V8 motorlarıyla uyumlu çalışır. Ancak V8 kullanıyorsanız v8-jsi adaptörünü eklemeniz gerekir.
eski Native Modules (Bridge) yapısını tamamen bırakmalı mıyım?
Eğer uygulamanız sadece kullanıcı profili güncellemek, kamera açmak veya bir analitik event’i fırlatmak gibi düşük frekanslı işler yapıyorsa, Bridge yeterlidir ve JSI kodunun karmaşıklığına girmek gereksiz maliyettir. Optimizasyon ihtiyacı sadece veri transferi milisaniyelere sıkıştığında (Video/Audio streaming, lokal veritabanı, sensörler) başlamalıdır.
C++ standartlarında sınırlandırmalarım var mı?
React Native (0.68+ sürümleri) varsayılan olarak C++17 kullanır. CMake ve Podspec dosyalarınızda standardı -std=c++17 olarak sabitleyerek modern C++ yeteneklerini kullanabilirsiniz. Standart kütüphane (STL) bağımlılıklarını kullanmakta özgürsünüz.
Sonuç
Asenkron Bridge mimarisinden JSI ve C++ Host Object mimarisine geçiş, React Native projelerinizde saf Native (Swift/Kotlin) performansına ulaşmanızı sağlayan en önemli kırılma noktalarından biridir. Seri JSON dönüşümleri ve kuyruklama limitlerini ortadan kaldırarak JS Thread ve Native Thread arasında 15 mikrosaniye tepki süreli, doğrudan senkron bir bağlantı kanalı kurmayı başardık.
Bir sonraki adımınız, uygulamanızda eski Bridge mimarisini en çok yoran veri akışını (örneğin AsyncStorage yerine C++ tabanlı JSI uyumlu MMKV entegrasyonu) analiz edip, bu düğüm noktalarını kendi yazdığınız özel JSI modülleriyle değiştirmek olmalıdır.
Bunları da beğenebilirsiniz

Javascript ile Küfür Ayıklama Fonksiyonu
Merhabalar, daha önceki yazımda php ile küfür engelleme fonksiyonu yazmıştık, bu yazımda ise aynı işlevi javascript ile yapacağız. Yazacağımız fonksiyon javascript ile girilen metni kontrol…

Konsol Portfolyo Tasarımım Yayında
Merhabalar, bu yazımda sizlere mevcut olarak kullandığım portfolyo tasarımımın alternatifi olarak bir de konsol tasarımını tamamladığımı duyurmak istiyorum. Yeni tasarlamış olduğum bu konsol portfolyoda belirli…

Edge Cihazlarda YOLOv8 ile Gerçek Zamanlı Nesne Tespiti: Docker ve NVIDIA Jetson Üzerinde Performans Optimizasyonu
Bu kapsamlı rehberde, YOLOv8 modelini kullanarak NVIDIA Jetson edge cihazlarda gerçek zamanlı nesne tespitini nasıl optimize edeceğinizi öğreneceksiniz. Docker ve TensorRT entegrasyonuyla performansı zirveye taşıyın.