修补Perl:加载返回false的模块
[更新:这现在是Perl 7的问题 https://github.com/Perl/perl5/issues/17921]
如果你已经编写了一段时间的Perl,你可能遇到过这个异常:“Foo.pm没有返回true值”。这是require函数的一个奇特特性:模块必须返回一个true值,否则Perl将其解释为失败
该文件必须作为最后一个语句返回true,以指示初始化代码的执行成功,因此通常在文件末尾使用“1;”,除非你确信它将以其他方式返回true。但最好是放置“1;”,以防你添加更多语句。
perlfunc
我觉得这个功能没有用:如果模块初始化失败,它可以用有意义的错误消息调用die,而不是返回false,Perl以通用消息崩溃。我敢打赌,在遇到这个异常的大多数情况下,是因为程序员忘记在他们的模块代码中添加一个true值。如果Perl的一个信条是优化常见情况,那么在require返回false时崩溃似乎并不合适。
Perl的许多其他功能已被其他语言采用,从正则表达式语法到use strict(你好,JavaScript!)。但我不知道有任何语言复制了这个功能——也许是因为它不太有用?
允许require返回false
那么我该怎么做呢?为了允许加载不返回true值的模块,需要更改Perl源代码。我偶尔会翻阅源代码来帮助更好地理解Perl解释器API,但以前我从未更改过源代码……直到现在!
我做的第一件事是分支Perl 源代码。我开始在代码中搜索异常消息“没有返回true值”,果然在pp_ctl.c中的函数S_pop_eval_context_maybe_croak中找到了它。当eval完成时(require evals要加载的代码)会调用这个函数以清理堆栈,并且可选地,如果遇到异常则崩溃。它接受0到2之间的数字:0表示“不要崩溃”,1表示“崩溃:require没有返回true值”,2表示“崩溃:require触发了编译错误”。
接下来,我搜索了S_pop_eval_context_maybe_croak的调用者,发现只有一个调用者将1传递给该函数,这是“leave eval”操作码声明,其中包含以下逻辑
failed = CxOLD_OP_TYPE(cx) == OP_REQUIRE
&& !(gimme == G_SCALAR
? SvTRUE_NN(*PL_stack_sp)
: PL_stack_sp > oldsp);
...
/* pop the CXt_EVAL, and if a require failed, croak */
S_pop_eval_context_maybe_croak(aTHX_ cx, NULL, failed);
这C代码刺痛了你的眼睛吗?欢迎来到Perl内部世界!它所做的就是检查当前的Perl上下文是否是require op,然后如果返回上下文是标量,检查堆栈顶部的值是否为true,否则(对于列表上下文)检查堆栈计数是否增加。
所以我删除了failed代码块,并将S_pop_eval_context_maybe_croak的调用更改为始终传递0。
然后我编译了源代码
$ ./Configure -des -Dusedevel -Dprefix=$HOME/blead-perl
$ make -j4
最后,我创建了一个名为“Foo.pm”的模块,其中只包含:0;
。然后我尝试使用新编译的Perl加载它
$ ./perl -I. -e 'require "Foo.pm"'
我没有看到“Foo.pm没有返回true值”的错误,太好了!
将其作为“功能”
我认为P5P(维护Perl源代码的团体)不会接受我的修改。一方面,任何依赖于require返回false功能的代码,在下一个Perl版本中都会被破坏。现在引入新行为的首选方法是用特性声明。所以我撤销了之前的修改,并尝试将允许require返回false作为一个特性来实现。
Perl源代码中有一个名为regen/feature.pl
的便捷工具,它负责生成实现特性标志所需的C和Perl代码。你只需要将新特性的名称添加到regen/feature.pl
中,然后运行脚本将其添加到Perl源代码。
我将“require_false”特性添加到regen/feature.pl
中并运行了脚本,产生了这些更改。这向header.h
中添加了宏FEATURE_REQUIRE_FALSE_IS_ENABLED
,我将在稍后用它来检查特性是否启用。此外,由于require_false
是这组中名称最长的特性,脚本还更新了宏MAX_FEATURE_LEN
的值,这样Perl的解析器在检查特性名称时就会比较正确的字节数。
添加测试
在这个阶段,我已经创建了一个新的特性,但还没有在任何地方使用它。这似乎是更新源代码测试以检查特性是否工作的好时机:一开始它不会工作,但在我为特性工作的时候,我可以快速重新编译并运行测试来检查。
在Perl附带的一套测试中搜索,我发现t/comp/require.t测试了require
在加载模块时是否做正确的事情。Perl源代码测试套件的有趣之处在于它们不能使用我们用于测试的通用工具,如Test::More
,它们只是打印TAP输出,让测试框架来处理。
我更新了t/comp/require.t以启用新特性,并测试加载返回false值的模块。我还测试了当特性启用时,不会忽略编译错误。因为声明是范围有限的,我必须将测试写在块内,而且我也不能使用测试辅助函数do_require
来处理所有事情,因为它会在不同的作用域中执行。
{
print "use feature 'require_false;'\n";
use feature 'require_false';
write_file('bleah.pm', '0;');
%INC = ();
eval { require "bleah.pm" };
$i++;
print "not " if $@ =~ /did not return a true value/;
print "ok $i - require loads module returning 0\n";
write_file('bleah.pm', 'die "foobar";');
%INC = ();
eval { require "bleah.pm" };
$i++;
print "not " unless $@ =~ /foobar/;
print "ok $i - require throws compile error\n";
}
注意在每个测试之前都会清除%INC
,因为Perl不会重新加载它在%INC
中找到的模块。然后我通过make
重新编译Perl,并运行测试。
$ ./perl -I. -MTestInit t/comp/require.t
1..60
ok 1 - require 5.005 try 1
...
# use feature 'require_false';
not ok 59 - require loads module returning 0
ok 60 - require throws compile error
正如预期的那样,模块没有被加载。顺便说一句,TestInit
是一个有用的模块,可以加载以避免运行整个Perl源代码测试套件,当您只想测试某些行为时,这可以节省很多时间(我多次运行make -j4 && ./perl -I. -MTestInit t/comp/require.t
)。
使用特性
在我之前的修改中,我更新了pp_ctl.c
中的leave eval op声明,这似乎是在其中添加检查特性是否启用或否的地方,并告诉S_pop_eval_context_maybe_croak
是否崩溃。然而,我发现这不起作用,即使特性已启用,FEATURE_REQUIRE_FALSE_IS_ENABLED
也始终为false。
我认为这是因为以 PP(pp_leaveval)
开头的行是通过 PP
宏声明了一个新的操作 - 它不是 C 函数声明。相反,我尝试将逻辑添加到 S_pop_eval_context_maybe_croak
本身,并且它工作了。这个更改证明是非常简单的。我导入 feature.h
,然后在 do_croak
赋值中添加了一个逻辑条件,检查 FEATURE_REQUIRE_FALSE_IS_ENABLED
是否已启用。我之前解释了 action
变量:如果它的值为 2,则表示有编译错误,我们仍然希望允许崩溃。
S_pop_eval_context_maybe_croak(pTHX_ PERL_CONTEXT *cx, SV *errsv, int action)
...
do_croak = action && (CxOLD_OP_TYPE(cx) == OP_REQUIRE) &&
(!FEATURE_REQUIRE_FALSE_IS_ENABLED || action == 2);
...
剩下的只是重新编译并再次运行测试。
$ ./perl -I. -MTestInit t/comp/require.t
1..60
ok 1 - require 5.005 try 1
...
# use feature 'require_false';
ok 59 - require loads module returning 0
ok 60 - require throws compile error
所有的 require
测试都通过了,耶!
总结
我计划从 P5P 获得对这个更改的反馈:在实现方面,我不确定我是否通过将 feature.h
导入 pp_ctl.c
违反了未成文的规则。如果我违反了,另一种实现相同功能的方法是声明一个新的私有标志用于 require 操作,并在 perly.y
语法创建新 require 操作的部分中设置它(每次它在 Perl 代码中遇到 require
时)。然后可以在 pp_ctl.c
中检查该标志,而不是启用功能宏。
尽管这个更改相对安全 - 模块可以自由地返回 true 值,但我担心它还不够有用,不值得成为一个功能。我很难想象明年的 5.30 用户会热衷地将这个功能添加到他们的代码中。也许不值得更改?
另一种可能的实现方法是废弃异常:“Foo.pm 没有返回 true 值。警告:此行为已弃用,将在 Perl 未来版本中删除”。这有一个优点,即不会添加新功能(更多代码,版本复杂性),并且给使用该功能的用户提供关于其删除的提前警告。当该行为被删除时,将导致 Perl 源代码中的 更少 代码,这在我看来是一个胜利。
与 Perl 源代码一起工作可能会让人感到畏惧:它是一大堆高级 C 代码的集合,这些代码大量使用宏。源代码的约定也可能很晦涩:函数、宏和变量名通常遵循一种逻辑但不可直观的命名格式。以前我曾不得不在纸上 literally 写出调用链以保持跟踪。但是,改变 Perl 的行为以适应你的口味是非常令人满意的。想象一下,有了这种力量,你会改变什么?这可能不是一条容易的路,但有价值的东西很少容易获得,而且你可能会在这个过程中更多地了解 Perl 的内部工作原理,并学到一些新的 C 编程技巧。
有时我不得不 literally 在纸上写出调用链来保持跟踪。但是,改变 Perl 的行为以适应你的口味是非常令人满意的。想象一下,有了这种力量,你会改变什么?这可能不是一条容易的路,但有价值的东西很少容易获得,而且你可能会在这个过程中更多地了解 Perl 的内部工作原理,并学到一些新的 C 编程技巧。
标签
反馈
这篇文章有什么问题吗?请通过在 GitHub 上打开一个问题或拉取请求来帮助我们。