Internal Indirection

随着在 Raku 中写的 shell 脚本越来越多,我意识到我以一种非常间接的方式调用 MAIN。我在想,能不能找到一种减少间接性的方法,以摆脱额外的进程和至少一些 Proc::Async 的开销。首先我们需要一个脚本来调用。

#! /usr/bin/env raku

constant \frame = gather while my $frame = callframe($++) {
    take $frame
}

sub MAIN {
    say frame[0];

    .note for lines;

    exit 42;

    say 'alive';
}

这个脚本会打印其调用者。然后通过 lines 间接读取 $*IN 并阻塞。如果进程能通过 exit,它将打印 alive。请注意,MAIN 是这个编译单元的最后一条语句。这让我们可以对脚本进行 slurp 和 EVAL。

my &main := $path.IO.slurp.&EVAL;

这就是了! 我们可以从另一个脚本中调用 MAIN。不过有几个注意事项。我们可以从一个脚本到另一个脚本的数据管道,因为它们共享 $*IN$*OUT。另外,STDERR 可能会让人感到困惑。exit 的语义可能是错误的。很可能我们想让外层脚本继续运行。捕获内脚本的返回值很容易,exitcode 就不容易了。

由于我们调用的是函数词法的,可以帮助解决大部分的问题。在 Raku 中,我们不能轻易地捕获 c-land exit。但我们可以防止它被调用。

my &*EXIT = sub ($exitcode) {
    CapturedExitcode.new(:$exitcode).throw;
}

这里的 exitcode 被封装在一个异常中,所以我们可以从 MAIN 中提取它。这个子程序可能会以返回的方式退出,所以我们也要捕捉到它。

$out.exitcode = main();

CATCH {
    when CapturedExitcode {
        $out.exitcode = .exitcode;
    }

    default {
        say .^name, ': ', .message;
        put .backtrace».Str
    }
}

现在我们需要处理 $*IN$*OUT。由于目标脚本只是调用 lines 和转发到 $*ARGFILES.lines,我们可以使用 Channel。其中 Channel 是存储 exitcode 的好地方。

my $out = Channel.new but role :: { has $.exitcode is rw; };
my $in = Channel.new but role :: { method put(Any:D \value){ self.send: value ~ $?NL } };

由于 lines 需要一个 Str,我们提供了熟悉的 put 方法。其他方法如 say 也会进入匿名角色。当内部 MAIN 终止时,我们要关闭 $out 通道。我们可以在一个 LEAVE 块中进行。整个过程可以包装成一个 sub,它为外脚本提供了一个很好的接口。

my ($out, $in) = embed-script('./script.raku');

start {
    for ^10 {
        $in.put: $++;
    }
    $in.close;
}

.print for $out.list;

say $out.exitcode;

处理 multi MAIN 是很棘手的。如果脚本中的最后一条语句是 &MAIN,那么它将指的是调度器。对于最后的任何多候选者,我们只能通过下降到 nqp-land 来掌握 proto。

my &main := $path.IO.slurp.&EVAL;

use nqp;
my &disp = &main.multi ?? nqp::getattr(&main, Routine, '$!dispatcher') !! &main;

然后我们可以调用 disp() 来调度到正确的 MAIN 候选。我不知道这是不是很脆弱。每一个 multi 例程都有一个 dispatcher。然而 Routine.dispatcher 并没有被 CORE 暴露出来。

整个例子可以在这里找到。

避开 Proc::Async 确实加快了不少速度。因为我们可以使用 Channel,所以在移动数据的时候,我们不必进行字符串化和解析输出。在这种情况下,被调用的 MAIN 需要配合,因此需要知道它没有被运行时调用。我们可以引入一个新的词法,或者对 Channel 检查 $*IN。还可以选择检查 EVAL 的调用框架。

constant \frame = gather while my $frame = callframe($++) {
    take $frame
}

say 'indirect' if any frame».gist».starts-with('EVAL');

与 Perl 5 颇为不同的是,对于 Raku,我从不怎么使用 EVAL。不是因为我害怕 - 代码生成的理由不多。毕竟,在我们两个人之间,我一直是比较邪恶的双胞胎。

更新

正如 jnthn 指出的 Routine.dispatcher 已经暴露了。因此,我们可以很好地避开 nqp:

my &disp = &main.multi ?? &main.dispatcher() !! &main;

到目前为止,还不清楚是否应该规范化,从而记录下来。

Raku 

comments powered by Disqus