Merhaba arkadaşlar;
Daha önce yazdığım Ergosfare gibi bir mediator kütüphanesini Rust için yazmaya karar verdim. Ergosfare yapısı itibarıyla handler tabanlı çalışan bir mimari ve bu handler’ların etkili şekilde çalışabilmesi için güçlü bir bağımlılık çözümleme (dependency injection) sistemine ihtiyaç duyuyor. Rustta bu tarz hazır bir yapı bulunmadığı için elle yazmaya karar verdim.
Aşağıda ortaya çıkarttığım prototipi (Singularity) tanıtan örnekler ve detaylar yer alıyor. Fikirlerinize, önerilerinize ve eleştirilerinize açığım.
Singularity nedir?
Compile-time çalışan reflection’sız, stateless DI mekanizmasıdır
Bağımlılıkları otomatik algılar ve recursive olarak çözer
Makrolar ile zahmetsiz kullanım sağlar
Runtime overhead yoktur – Singularity container herhangi bir allocation yapmaz — ne heap ne stack. Allocation yalnızca instantiate edilen servis (ve varsa bağımlılıkları) oluşturulurken gerçekleşir. Bu da zaten Rust’ın doğal nesne oluşturma maliyetidir; DI mekanizmasından ek yük gelmez. Dependency graph monomorphization aşamasında compaier tarafından statik olarak oluşturulur
Kullanımı ne kadar basit?
Elle implementasyon (orijinal yaklaşım)
İlk oluşturduğum yapı bu şekildeydi:
struct Config {
host: String, // localhost
port: u16, // 5432
}
impl Injectable for Config {
type Deps = ();
pub fn inject(_: Self::Deps ) -> Self {
Self {
host: "localhost".to_string(),
port: 5432
}
}
}
struct Tcp(Config);
impl Injectable for Tcp {
type Deps = (Config,);
pub fn inject(config: Self::Deps ) -> Self {
Self(config)
}
}
fn main() {
let container = Container::new();
let svc = container.resolve::<Tcp>();
println!("{:?}", svc);
}
Konsol çıktısı:
Tcp(Config { host: "localhost", port: 5432 })
macro_rules! (declarative macro)
Bu da daha sonra yazdığım ikinci yaklaşım:
use singularity::container::*;
injectable!(() => struct Config { host: String = "localhost".to_string(), port: u16 = 5432 });
injectable!((config: Config) => struct Tcp());
fn main() {
let container = Container::new();
let svc = container.resolve::<Tcp>();
println!("{:?}", svc);
}
Konsol çıktısı:
Tcp(Config { host: "localhost", port: 5432 })
Bu macro ile derleme sırasında (lexical expansion aşamasında) yukarıdaki manuel impl versiyonuna dönüştürülür. Compiler açısından ikisi arasında herhangi bir fark yoktur. Lexical expansion compiler’dan hemen önce gerçekleştiği için compiler her zaman bunun generate ettiği kodu görür.
Tipik macro_rules! açılımı yani — herhangi bir dış paket yok, tamamen Rust’ın kendi macro_rules! sistemiyle.
proc_macro (derive & attribute)
Bu versiyonu geliştirirken biraz çekinceliydim, çünkü token tabanlı AST parsing gerekiyordu. Rust standard kütüphanesi bunun için hazır araç sunmadığı için projede ilk defa dış bağımlılıklara (syn, quote, proc-macro2) ihtiyaç duydum.
use singularity::container::*;
#[derive(Debug, Injectable)]
struct Config {
#[inject(|| "localhost".to_string())]
host: String,
#[inject(|| 5432)]
port: u16,
}
#[derive(Debug, Injectable)]
struct Tcp(Config);
fn main() {
let container = Container::new();
let svc = container.resolve::<Tcp>();
println!("{:?}", svc);
}
Konsol çıktısı:
Tcp(Config { host: "localhost", port: 5432 })
Bu kodda aynı şekilde lexical expansion aşamasında ilk versiyon gibi bir yapıya expand edilir ve gerekli Injectable trait implementasyonu otomatik olarak üretilir.
-
#[derive(Injectable)]→ struct’ın bağımlılıklarını analiz eder ve gerekli trait implementasyonunu compile-time’da üretir. Makro yalnızca metaprogramming aracıdır – runtime’da çalışmaz, compile aşamasında kod generatörü olarak çalışır.Injectabletrait’i elle yazmak yerine otomatik olarak implement eder.- Reflection veya runtime işlem yapmaz.
- İstenirse macro kullanmadan elle
Injectabletrait’i implement edilebilir.
-
#[inject(|| expr)]→ dışarıdan factory tabanlı bağımlılık alabilmemizi sağlar. -
container.resolve::<T>()→ bağımlılık çözümlemesi runtime’da değil, monomorphization aşamasında gerçekleşir. -
Container durumsuzdur (stateless) → hiçbir servis saklanmaz, kayıt tutulmaz.
-
Dependency graph yalnızca generic tür sistemi üzerinden çıkarılır — runtime veri yapısı gerekmez.
-
Çözümleme aşamasında herhangi bir memory allocation yapılmaz.
-
Allocation yalnızca servis oluşturulurken, Rust’ın doğal kurallarına göre yapılır.
Her üç versiyon da runtime’da tamamen aynı performansa sahip — fark sadece nasıl yazdığınız. En minimal olanı elle olan, en ergonomik olan ise derive macro.
Özetlemek gerekirse:
| Yaklaşım | Runtime etkisi | Derleme zamanı iş yükü | Dış bağımlılık |
|---|---|---|---|
| Elle implementasyon | Yok | Yok | Yok |
macro_rules! |
Yok | Orta | Yok |
#[derive(...)] |
Yok | Yüksek (AST analizi) | (syn, quote) |