深入了解组件
组件为 Starknet 合约提供了强大的模块化。但在这个魔术的背后是什么原理呢?
本章将深入探讨编译器的内部结构,以解释 实现组件可组合性的机制。
嵌入式实现入门
在深入研究组件之前,我们需要了解 embeddable impls 。
一个实现了Starknet 接口trait(标有#[starknet::interface]
)的impl是可嵌入的。
可嵌入的impl可以注入到任何合约中,这会添加新的入口点以及改变合约的 ABI。
让我们看一个示例来看看这个实际运作的过程:
#![allow(unused)] fn main() { #[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>; } }
通过嵌入 SimpleImpl
,我们在合约的 ABI 中向外部公开 ret4
。
现在我们对嵌入机制更加熟悉了,我们可以看到组件如何在此基础上构建。
内部组件:泛型实现
回想一下组件中使用的 impl 块的语法:
#![allow(unused)] fn main() { #[embeddable_as(Ownable)] impl OwnableImpl< TContractState, +HasComponent<TContractState> > of super::IOwnable<ComponentState<TContractState>> { }
关键点:
-
OwnableImpl
要求底层合约实现HasComponent<TContractState>
trait。 这个trait在合约中使用组件的时候,会由component!()
宏自动生成。编译器将生成一个封装
OwnableImpl
中任何函数的 impl、 用self:TContractState
替换掉中的self: ComponentState<TContractState>
参数,其中对组件状态的访问是通过HasComponent<TContractState>
trait中的get_component
函数来进行的。编译器会为每个组件生成一个
HasComponent
trait。该trait 定义了连接泛型合约与实际TContractState
和ComponentState<TContractState>
之间的接口。#![allow(unused)] fn main() { // 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); } }
在我们的上下文中,
ComponentState<TContractState>
是一个特定的ownable组件的类型。 它拥有着基于ownable_component::Storage
定义的变量成员。 从泛型TContractState
转移到ComponentState<TContractState>
使得我们可以将Ownable
嵌入任意合约。反方向的转移(即ComponentState<TContractState>
转移到ContractState
)对于外部依赖是很有用的。 详细可见组件依赖 章节中的依赖于一个IOwnable
实现的Upgradeable
组件的例子。简而言之,我们应该考虑上述
HasComponent<T>
的实现: "合约的状态 T 具有可升级组件"。 -
Ownable
被标注为embeddable_as(<name>)
属性:embeddable_as
与embeddable
类似;它只适用于starknet::interface
trait的 "impls", 并允许将这个impl嵌入一个合约模块。也就是说,embeddable_as(<name>)
在组件的上下文中还有另一个作用。 最终,当在某个契约中嵌入OwnableImpl
时,我们希望得到一个具有以下函数的 impl:#![allow(unused)] fn main() { fn owner(self: @TContractState) -> ContractAddress; fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress); fn renounce_ownership(ref self: TContractState); }
请注意,在开始使用函数接收泛型
ComponentState<TContractState>
时,我们期望的是一个接受ContractState
的函数。 这就是embeddable_as(<name>)
的用武之地。要了解全景, 我们需要知道编译器为embeddable_as(Ownable)
注解生成的 impl 是什么:#![allow(unused)] fn main() { #[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, ) } } }
请注意,由于有了
HasComponent<TContractState>
的impl, 编译器才能够将我们的函数封装在一个不需要直接知道ComponentState
的新的impl中。 在合约中写上embeddable_as(Ownable)
时,就意味着Ownable
将是被嵌入合约中来实现所有权功能的impl。
合约集成
我们已经了解了泛型impl如何实现组件的可重用性。接下来,让我们看看合约是如何集成组件的。
合约使用 impl alias 来实例化组件的泛型 impl
替换为该合约的具体ContractState
。
#![allow(unused)] fn main() { #[abi(embed_v0)] impl OwnableImpl = ownable_component::Ownable<ContractState>; impl OwnableInternalImpl = ownable_component::InternalImpl<ContractState>; }
上面的代码使用了 Cairo impl 嵌入机制和 impl 别名语法。
我们正在用具体类型的ContractState
来实例化泛型 OwnableImpl<TContractState>
回想一下,OwnableImpl<TContractState>
具有HasComponent<TContractState>
泛型 impl 参数。
此实现的trait 由 component!
宏生成。
请注意,这只可以使用在可以实现这个trati的合约上, 因为只有这种合约知晓合约state 和组件 state。
这会将所有内容粘合在一起,以将组件逻辑注入到合约中。
关键要点
- 可嵌入的实现允许通过组件注入来添加入口点和改变合约 ABI。
- 档组件被用于合约中时,编译器会自动生成
HasComponent
tarit实现 这在合约状态和组件状态搭了一座桥,以实现双方互动。 - 组件以通用的、与合约无关的方式封装可重用的逻辑。
合约通过 impl 别名集成组件,并通过生成的
HasComponent
trait来访问组件。 - 组件基于可嵌入的实现构建,通过定义通用组件逻辑 可以集成到任何想要使用该组件的合约中。 实现别名使用合约的具体存储来实例化这些泛型实现类型。