深入了解组件

组件为 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 定义了连接泛型合约与实际 TContractStateComponentState<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_asembeddable类似;它只适用于 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来访问组件。
  • 组件基于可嵌入的实现构建,通过定义通用组件逻辑 可以集成到任何想要使用该组件的合约中。 实现别名使用合约的具体存储来实例化这些泛型实现类型。
Last change: 2023-11-04, commit: c56dc86