ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Solidity 函数修饰符(Function Modifiers)权威指南:从 `_;` 占位符语义到重入防护的源码级解析

Solidity 函数修饰符(Function Modifiers)权威指南:从 `_;` 占位符语义到重入防护的源码级解析 Solidity 函数修饰符Function Modifiers权威指南从_;占位符语义到重入防护的源码级解析【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity函数修饰符Modifier是 Solidity 中用来以声明式方式改变函数行为的内置特性最常见的用法是在函数执行前自动完成条件校验。本文以官方文档 docs/contracts/function-modifiers.rst 为主体结合编译器的语法解析、类型检查与代码生成实现如 Parser.cpp、TypeChecker.cpp、ContractCompiler.cpp以及语义测试用例系统讲解修饰符的语法、_;占位符的展开机制、参数传递、继承与覆盖规则、库内修饰符限制并给出可复用的权限控制与重入锁实战模板。读完本文你将能写出正确、安全且符合 Solidity 语义的修饰符并理解编译器究竟是如何展开它们生成 EVM 字节码的。修饰符是什么声明式地改变函数行为修饰符可以在函数执行之前、之后或中间插入一段公共逻辑从而以声明方式declarative way改变函数行为。最典型的场景是在函数体真正执行前自动检查某个条件不满足则回滚。修饰符是合约的可继承属性inheritable properties可以被派生合约覆盖override但前提是被覆盖的修饰符标记了virtual。关于覆盖的完整细节见 Modifier Overriding。从编译器的数据结构看修饰符在 AST抽象语法树中是一个独立的节点类型ModifierDefinition继承自CallableDeclaration可调用声明其可见性被固定为internal并拥有isVirtual标记与覆盖说明符定义节点ModifierDefinitionlibsolidity/ast/AST.h调用节点ModifierInvocation表示函数声明中的costs(price)这类修饰符调用语法入门带_;占位符的修饰符定义修饰符的语法与函数类似但没有返回参数函数体被插入到修饰符体中特殊的占位符符号_;出现的位置。下面是文档中的经典例子owned合约定义了一个onlyOwner修饰符它在函数体执行前校验调用者是否为合约 owner校验通过后执行_;处的函数体否则抛出异常回滚// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.7.1 0.9.0; contract owned { constructor() { owner payable(msg.sender); } address payable owner; // This contract only defines a modifier but does not use // it: it will be used in derived contracts. // The function body is inserted where the special symbol // _; in the definition of a modifier appears. // This means that if the owner calls this function, the // function is executed and otherwise, an exception is // thrown. modifier onlyOwner { require( msg.sender owner, Only owner can call this function. ); _; } }修饰符也可以接收参数例如下面带uint price参数的costs修饰符当msg.value不低于价格时才继续执行函数体contract priced { // Modifiers can receive arguments: modifier costs(uint price) { if (msg.value price) { _; } } }语法层面的解析规则从 Parser::parseModifierDefinition 可以看出修饰符的完整语法形态先解析可选的文档注释parseStructuredDocumentation遇到modifier关键字后解析标识符名称若紧跟(则解析参数列表允许storage/memory等位置说明符否则参数列表为空循环解析可选的override与virtual关键字重复指定会触发9102_error/2662_error解析错误若当前 token 不是分号;则解析函数体代码块否则表示该修饰符只有声明没有实现必须配合virtual。解析器还维护了一个m_insideModifier标志通过ScopeGuard自动复位用于在后续分析阶段区分代码是否位于修饰符内部。组合使用多个修饰符按顺序嵌套展开多个修饰符作用于同一个函数时以空白分隔列出并按照列出的顺序依次求值evaluated in the order presented。Register合约演示了从基类继承onlyOwner、以及同时使用多个基类修饰符的写法contract Register is priced, owned { mapping(address bool) registeredAddresses; uint price; constructor(uint initialPrice) { price initialPrice; } // It is important to also provide the // payable keyword here, otherwise the function will // automatically reject all Ether sent to it. function register() public payable costs(price) { registeredAddresses[msg.sender] true; } // This contract inherits the onlyOwner modifier from // the owned contract. As a result, calls to changePrice will // only take effect if they are made by the stored owner. function changePrice(uint price_) public onlyOwner { price price_; } }注意register必须同时声明payable否则合约会自动拒绝所有发送给它的 Ether——即使修饰符本身已经检查了msg.value price。代码生成修饰符如何被展开修饰符并不是运行时调用而是在编译期被内联展开进函数体。核心实现在 ContractCompiler::appendModifierOrFunctionCodelibsolidity/codegen/ContractCompiler.cpp编译器维护一个修饰符深度计数器m_modifierDepth当深度未达到m_currentFunction-modifiers().size()时取出当前深度的ModifierInvocation解析修饰符引用的声明若存在VirtualLookup::Virtual查找则通过resolveVirtual(m_context.mostDerivedContract())解析到最终派生合约中的实际实现逐个将调用处的实参表达式编译求值compileExpression并绑定到修饰符形参依次编译第 0 个修饰符体 → 第 1 个修饰符体 → … → 函数体即多个修饰符按声明顺序形成嵌套包裹结构每个被展开的块结束处都对应一个 return tag块与块之间通过标签跳转衔接。这解释了为什么_;可以出现多次每一次出现都会把函数体替换进去而函数返回的是最后一次出现处的返回值见下文。_;占位符函数体插入位置与多次出现语义_;placeholder statement占位符语句用于指明被修饰函数体应该插入的位置。需要注意占位符操作符_与变量名中作为首尾字符的下划线纯风格选择是完全不同的概念_符号可以在修饰符中出现多次每次出现都会被替换为函数体且函数返回最后一次出现处的返回值修饰符不能隐式访问或修改它所修饰函数的参数与返回值这些值只能在调用点显式传入。显式 return 的精确语义文档特别强调了两条容易踩坑的规则修饰符或函数体内的显式return只退出当前所在的修饰符体或函数体。返回变量会被赋值然后控制流继续从前一个修饰符中_;之后的位置执行。修饰符中的return;不影响函数返回值。但修饰符可以选择完全不执行函数体——此时返回变量被设置为其默认值见 default values 相关说明就像函数体为空一样。警告在 Solidity 更早的版本中带修饰符的函数里return语句的行为与现在不同迁移旧代码时需注意版本差异。重入锁Mutex的经典写法文档给出的Mutex合约正是利用_;之后的代码在函数返回后仍会执行这一特性实现的防重入锁contract Mutex { bool locked; modifier noReentrancy() { require( !locked, Reentrant call. ); locked true; _; locked false; } /// This function is protected by a mutex, which means that /// reentrant calls from within msg.sender.call cannot call f again. /// The return 7 statement assigns 7 to the return value but still /// executes the statement locked false in the modifier. function f() public noReentrancy returns (uint) { (bool success,) msg.sender.call(); require(success); return 7; } }在f内部对外发起msg.sender.call()时若目标地址回调用f会因为locked仍为true而触发require失败。而f中的return 7只会退出函数体随后控制流回到修饰符中_;之后继续执行locked false完成解锁——这正是显式 return 后控制流继续语义的实战体现。源码佐证return 与占位符的处理return的只退出当前块语义由编译器的块级 return tag 机制保证每个被展开的代码块修饰符体或函数体都有独立的m_returnTags条目见 ContractCompiler.cppreturn 只跳转到当前块末尾的标签。语义测试 break_in_modifier.sol、continue_in_modifier.sol、function_modifier.sol 等用例覆盖了修饰符内的跳转与控制流行为。修饰符内部出现msg.value/callvalue()时编译器会要求该函数声明为payable或内部函数否则报错——实现见 ViewPureChecker 的修饰符可变性mutability推断。访问规则C.m引用、库修饰符与可见性限制文档对修饰符的访问范围给出了明确限定若想访问合约C中定义的修饰符m可以使用C.m进行静态引用不经过虚查找只能使用当前合约或其基类合约中定义的修饰符修饰符也可以定义在库library中但其使用被限制为同一库的函数。类型检查阶段的实现印证了这些规则TypeChecker.cpp引用声明必须是修饰符或基类构造器否则报4659_error若修饰符定义在另一个合约中则该合约必须出现在linearizedBaseContracts线性化基类列表里否则报629行附近的错误Can only use modifiers defined in the current contract or in base contracts.参数数量不匹配报 Wrong argument count for modifier invocation参数类型不匹配报 Invalid type for argument in modifier invocation通过C.m静态引用一个未实现的修饰符会报错因为该引用合约中没有实现可调用。另外TypeChecker::endVisit(ModifierDefinition)TypeChecker.cpp规定库中的修饰符不能标记virtual因为库不能继承未实现只有声明没有函数体的修饰符必须标记virtual。库内修饰符的测试佐证语义测试 function_modifier_library.sol 演示了库内修饰符的真实用法库L定义了一个带storage引用参数s的修饰符mod库的内部函数libFun通过internal mod(s)使用它测试通过using L for *扩展后调用最终断言f() - 0x202s.v先被修饰符再被函数体 0x100合计0x202。这同时验证了修饰符形参可以携带storage位置引用这一能力。继承与覆盖Modifier Overriding修饰符支持类似函数的覆盖机制但不存在修饰符的重载overloading。覆盖规则要求被覆盖的修饰符必须声明virtual覆盖的修饰符必须声明override多重继承时必须列出所有直接基类如override(Base1, Base2)。文档 docs/contracts/inheritance.rst 给出了最小示例注意该小节标题标注为 deprecated因为modifier foo() virtual {_;}这种写法在文档写作时会产生弃用警告// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.6.0 0.9.0; contract Base { // This will report a warning (deprecation) modifier foo() virtual {_;} } contract Inherited is Base { modifier foo() override {_;} }从实现上看修饰符覆盖与函数覆盖共用resolveVirtual机制在 ContractCompiler.cpp 中VirtualLookup::Virtual查找会结合最派生合约mostDerivedContract解析出实际生效的修饰符实现而C.m静态引用则对应VirtualLookup::Static查找直接定位到C中声明的那个实现。参数求值时机与可见性修饰符实参可以是任意表达式且在该上下文中函数内可见的所有符号在修饰符内同样可见修饰符内引入的符号如形参、局部变量在函数体中不可见——因为它们可能因覆盖而被改变实参表达式在编译生成的代码中位于修饰符体执行之前求值见 ContractCompiler.cpp先compileExpression求值实参、压入栈再编译修饰符体。求值顺序有专门测试 evaluation_order.sol 覆盖而 access_through_contract_name.sol 与 access_through_module_name.sol 验证了C.m静态引用的行为。实践模板把规则组合成生产级代码将本文的规则汇总为一个可直接使用的模板// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.0; contract AccessControl { address public owner; constructor() { owner msg.sender; } /// 权限检查仅合约所有者可调用 modifier onlyOwner() { require(msg.sender owner, Only owner can call this function.); _; } /// 防重入函数执行期间锁定 modifier nonReentrant() { require(!locked, Reentrant call.); locked true; _; locked false; // 无论函数如何返回都会执行 } bool private locked; /// 组合两个修饰符先鉴权、再防重入 function sensitiveAction() public onlyOwner nonReentrant { // 业务逻辑 } }要点回顾顺序即嵌套onlyOwner nonReentrant表示onlyOwner的_;处插入整个nonReentrant包裹后的函数体payable与msg.value修饰符中读取msg.value会要求函数本身声明payablereturn的边界函数体return之后仍会执行修饰符_;之后的收尾代码这正是解锁/资源清理代码能可靠执行的原因继承要virtual/override派生合约覆盖修饰符时必须成对使用关键字。结语函数修饰符是 Solidity 中少有的编译期宏式语言特性理解_;占位符的文本替换语义、显式 return 的作用域边界、多修饰符的嵌套展开顺序是写出安全合约的基础。本文涉及的语义均有对应源码与测试支撑语法层面见 Parser.cpp类型与覆盖检查见 TypeChecker.cpp内联展开见 ContractCompiler.cpp行为验证见 test/libsolidity/semanticTests/modifiers/ 目录下的语义测试集合。需要更系统的语法参考时可继续阅读 docs/contracts/functions.rst、docs/contracts/inheritance.rst 与 docs/contracts/abstract-contracts.rst。【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表