Components: Under the Hood
Komponen memberikan modularitas yang kuat untuk kontrak Starknet. Tetapi bagaimana sebenarnya keajaiban ini terjadi di balik layar?
Bab ini akan menyelami ke dalam internal kompilator untuk menjelaskan mekanisme yang memungkinkan komposabilitas komponen.
Pengantar Mengenai Implementasi yang Dapat Disematkan
Sebelum menggali ke dalam komponen, kita perlu memahami implementasi yang dapat disematkan (embeddable impls).
Implementasi dari trait antarmuka Starknet (ditandai dengan #[starknet::interface]
) dapat dibuat embeddable. Implementasi yang dapat disematkan ini dapat disisipkan ke dalam kontrak apa pun, menambahkan titik masuk baru dan memodifikasi ABI dari kontrak tersebut.
Mari lihat contoh untuk melihat ini dalam aksi:
#[starknet::interface]
trait SimpleTrait<TContractState> {
fn ret_4(self: @TContractState) -> u8;
}
#[starknet::embeddable]
impl SimpleImpl<TContractState> of SimpleTrait<TContractState> {
fn ret_4(self: @TContractState) -> u8 {
4
}
}
#[starknet::contract]
mod simple_contract {
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl MySimpleImpl = super::SimpleImpl<ContractState>;
}
Dengan menyematkan SimpleImpl
, kita secara eksternal mengungkapkan ret4
dalam ABI kontrak.
Sekarang setelah kita lebih familiar dengan mekanisme penyematan, kita dapat melihat bagaimana komponen membangun dari ini.
Di Dalam Komponen: Implementasi Generik
Ingatlah sintaks blok impl yang digunakan dalam komponen:
#[embeddable_as(Ownable)]
impl OwnableImpl<
TContractState, +HasComponent<TContractState>,
> of super::IOwnable<ComponentState<TContractState>> {
Key points:
-
OwnableImpl
memerlukan implementasi dari traitHasComponent<TContractState>
oleh kontrak yang mendasarinya, yang secara otomatis dihasilkan dengan makrocomponent!()
ketika menggunakan komponen di dalam kontrak.Kompiler akan menghasilkan impl yang membungkus setiap fungsi dalam
OwnableImpl
, menggantikan argumenself: ComponentState<TContractState>
denganself: TContractState
, di mana akses ke status komponen dilakukan melalui fungsiget_component
dalam traitHasComponent<TContractState>
.Untuk setiap komponen, kompiler menghasilkan trait
HasComponent
. Trait ini mendefinisikan antarmuka untuk menjembatani antaraTContractState
aktual dari kontrak generik danComponentState<TContractState>
.// generated per component trait HasComponent<TContractState> { fn get_component(self: @TContractState) -> @ComponentState<TContractState>; fn get_component_mut(ref self: TContractState) -> ComponentState<TContractState>; fn get_contract(self: @ComponentState<TContractState>) -> @TContractState; fn get_contract_mut(ref self: ComponentState<TContractState>) -> TContractState; fn emit<S, impl IntoImp: traits::Into<S, Event>>(ref self: ComponentState<TContractState>, event: S); }
In our context
ComponentState<TContractState>
is a type specific to the ownable component, i.e. it has members based on the storage variables defined inownable_component::Storage
. Moving from the genericTContractState
toComponentState<TContractState>
will allow us to embedOwnable
in any contract that wants to use it. The opposite direction (ComponentState<TContractState>
toContractState
) is useful for dependencies (see theUpgradeable
component depending on anIOwnable
implementation example in the Components dependencies section).Secara singkat, seseorang seharusnya memandang implementasi dari
HasComponent<T>
di atas sebagai berkata: “Kontrak yang memiliki komponen yang dapat ditingkatkan untuk state T”. -
Ownable
dianotasi dengan atributembeddable_as(<name>)
:embeddable_as
is similar toembeddable
; it only applies to impls ofstarknet::interface
traits and allows embedding this impl in a contract module. That said,embeddable_as(<name>)
has another role in the context of components. Eventually, when embeddingOwnableImpl
in some contract, we expect to get an impl with the following functions:fn owner(self: @TContractState) -> ContractAddress; fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress); fn renounce_ownership(ref self: TContractState);
Perhatikan bahwa meskipun kita memulai dengan fungsi yang menerima tipe generik
ComponentState<TContractState>
, kita ingin berakhir dengan fungsi yang menerimaContractState
. Inilah saatnya menggunakanembeddable_as(<name>)
. Untuk melihat gambaran keseluruhan, kita perlu melihat impl yang dihasilkan oleh kompiler karena adanya anotasiembeddable_as(Ownable)
:
#[starknet::embeddable]
impl Ownable<
TContractState, +HasComponent<TContractState>, impl TContractStateDrop: Drop<TContractState>,
> of super::IOwnable<TContractState> {
fn owner(self: @TContractState) -> ContractAddress {
let component = HasComponent::get_component(self);
OwnableImpl::owner(component)
}
fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress) {
let mut component = HasComponent::get_component_mut(ref self);
OwnableImpl::transfer_ownership(ref component, new_owner)
}
fn renounce_ownership(ref self: TContractState) {
let mut component = HasComponent::get_component_mut(ref self);
OwnableImpl::renounce_ownership(ref component)
}
}
Perhatikan bahwa berkat adanya impl dari HasComponent<TContractState>
, kompiler dapat membungkus fungsi-fungsi kita dalam sebuah impl baru yang tidak langsung mengetahui tentang tipe ComponentState
. Ownable
, yang kita pilih saat menulis embeddable_as(Ownable)
, adalah impl yang akan kita sematkan ke dalam kontrak yang ingin memiliki kepemilikan.
Contract Integration
Kita telah melihat bagaimana impl generik memungkinkan penggunaan kembali komponen. Selanjutnya, mari kita lihat bagaimana sebuah kontrak mengintegrasikan sebuah komponen.
Kontrak menggunakan alias impl untuk menginisialisasi impl generik komponen dengan tipe konkret ContractState
dari kontrak.
#[abi(embed_v0)]
impl OwnableImpl = ownable_component::Ownable<ContractState>;
impl OwnableInternalImpl = ownable_component::InternalImpl<ContractState>;
Baris-baris di atas menggunakan mekanisme penyematan impl Cairo bersamaan dengan sintaksis alias impl. Kami menginisialisasi generic OwnableImpl<TContractState>
dengan tipe konkret ContractState
. Ingat bahwa OwnableImpl<TContractState>
memiliki parameter impl generik HasComponent<TContractState>
. Implementasi trait ini dihasilkan oleh makro component!
.
Perhatikan bahwa hanya kontrak yang menggunakan yang dapat mengimplementasikan trait ini karena hanya kontrak tersebut yang mengetahui tentang kedua status kontrak dan status komponen.
Ini menggabungkan semuanya untuk menyisipkan logika komponen ke dalam kontrak.
Key Takeaways
- Impl yang dapat disematkan memungkinkan penyisipan logika komponen ke dalam kontrak dengan menambahkan titik masuk dan memodifikasi ABI kontrak.
- Kompiler secara otomatis menghasilkan implementasi trait
HasComponent
ketika sebuah komponen digunakan dalam sebuah kontrak. Ini menciptakan jembatan antara status kontrak dan status komponen, memungkinkan interaksi di antara keduanya. - Komponen mengemas logika yang dapat digunakan kembali dengan cara yang generik dan tidak tergantung pada kontrak tertentu. Kontrak mengintegrasikan komponen melalui alias implementasi dan mengaksesnya melalui trait yang dihasilkan
HasComponent
. - Komponen membangun pada implementasi yang dapat disematkan dengan mendefinisikan logika komponen generik yang dapat diintegrasikan ke dalam setiap kontrak yang ingin menggunakan komponen tersebut. Alias implementasi menginisialisasi implementasi generik ini dengan tipe penyimpanan konkret dari kontrak.