在XS代码中安全地释放资源(在作用域退出时运行析构函数)。

10
我正在编写一个XS模块。我分配一些资源(例如malloc()SvREFCNT_inc()),然后进行涉及Perl API的操作,最后释放资源。这在普通的C语言中是没有问题的,因为C语言没有异常,但使用Perl API的代码可能会调用croak(),从而阻止正常的清理并泄漏资源。因此,除了相当简单的情况外,似乎不可能编写正确的XS代码。
当我自己调用croak()时,我可以清理到目前为止分配的任何资源,但我可能会调用直接引发croak()的函数,这将绕过我编写的任何清理代码。
伪代码以说明我的担忧:
static void some_other_function(pTHX_ Data* d) {
  ...
  if (perhaps) croak("Could not frobnicate the data");
}

MODULE = Example  PACKAGE = Example

void
xs(UV n)
  CODE:
  {
    /* Allocate resources needed for this function */
    Data* object_graph;
    Newx(object_graph, 1, Data);
    Data_init(object_graph, n);

    /* Call functions which use the Perl API */
    some_other_function(aTHX_ object_graph);

    /* Clean up before returning.
     * Not run if above code croak()s!
     * Can this be put into the XS equivalent of a  "try...finally" block?
     */
    Data_destroy(object_graph);
    Safefree(object_graph);
  }

那么如何在XS代码中安全地清理资源呢?我该如何注册一些析构函数,在抛出异常或从XS代码返回到Perl代码时运行?

到目前为止,我的想法和发现如下:

  • 我可以创建一个类,在析构函数中运行必要的清理操作,然后创建一个包含该类实例的可死亡 SV。在将来的某个时刻,Perl 将释放该 SV 并运行我的析构函数。不过,这似乎有些反向,肯定有更好的方法。

  • XSAWYERX 的 XS Fun 手册似乎详细讨论了 DESTROY 方法,但没有涉及源自 XS 代码内部的异常处理。

  • LEONT 的 Scope::OnExit 模块具有使用 SAVEDESTRUCTOR()SAVEDESTRUCTOR_X() 宏的 XS 代码。这些似乎没有文档说明。

  • Perl API 列出了 save_destructor()save_destructor_x() 函数作为公共但未记录的函数。

  • Perl 的scope.h头文件(由perl.h包含)声明了SAVEDESTRUCTOR(f,p)SAVEDESTRUCTOR_X(f,p)宏,没有进一步的解释。从上下文和Scope::OnExit代码来看,f是一个函数指针,p是一个将传递给f的 void 指针。_X 版本用于使用pTHX_宏参数声明的函数。

我用这种方法对吗?应该在适当的时候使用这些宏吗?它们是在哪个Perl版本中引入的?是否有关于它们使用的进一步指导?析构函数是在什么时候被触发的?大概与FREETMPSLEAVE宏相关的点上?


1
顺便说一句,好问题... - stevieb
2
Perl API通常只对无效输入抛出异常。如果您以可能导致异常的方式调用Perl API,请自行检查参数的有效性。唯一的例外是在调用Perl代码时,您可以使用G_EVAL标志 - nwellnhof
1
@nwellnhof,你忘记了致命警告(我之前提到过)。检查是否实际上得到了一个数字而不是使用SvIV并不实际。但是可以发出警告(未初始化或非数字),这可能会导致死机。 - ikegami
1
@ikegami 我正在分配临时数据结构(缓冲区,队列,图表),这些结构在执行XS函数期间需要。它们不会作为SV返回。它们没有受到某个常量的限制,因此无法进行堆栈分配。我添加了一段伪代码来说明我的问题结构。 - amon
1
我之前不知道SAVEDESTRUCTOR,但它似乎是一种可靠的运行清理代码的方式。然而,在我的内部C函数中,我不会使用Perl异常来处理错误。相反,我更喜欢返回错误代码,并在释放所有资源后使XSUB抛出异常。这对于许多典型情况应该已经足够了,例如从AVHV中提取数据。即使有一种方法可以让您库的用户插入一些奇怪的数据,使得Perl API函数抛出异常并导致内存泄漏,除非这是绝对关键的代码,否则我不会太担心。 - nwellnhof
显示剩余11条评论
1个回答

5
经过进一步的研究,发现 SAVEDESTRUCTOR 的确有文档记录,但在 perlguts 而不是 perlapi 中。其中确切的语义已经被记录下来。
因此我认为,SAVEDESTRUCTOR 应该用作清除的“finally”块,并且足够安全和稳定。
下面摘自 perlguts 中 Localizing changes,其中讨论了与 { local $foo; ... } 块相当的内容:

通过 Perl API 可以以类似的方式从 C 中实现这样的任务:创建一个伪块,并在其末尾自动撤销某些更改,无论是显式撤销还是通过非本地退出(即通过 die())。一对 ENTER/LEAVE 宏会创建一个类似于块的结构(请参见perlcall 中的 Returning a Scalar)。这样的结构可以专门用于某些重要的本地化任务,或者可以使用现有的结构(例如封闭 Perl 子例程/块的边界,或者用于释放 TMP 的现有一对)。(在第二种情况下,额外本地化的开销必须几乎可以忽略不计。)请注意,任何 XSUB 都会自动包含在一个 ENTER/LEAVE 对中。

在此类伪块内,可以使用以下服务:

  • [...]

  • SAVEDESTRUCTOR(DESTRUCTORFUNC_NOCONTEXT_t f, void *p)

    伪块结束时,将调用带有唯一参数 p 的函数 f

  • SAVEDESTRUCTOR_X(DESTRUCTORFUNC_t f, void *p)

    伪块末尾时,将使用隐式上下文参数(如果有)和 p 调用函数 f

该部分还列出了一些专门的析构函数,例如 SAVEFREESV(SV *sv)SAVEMORTALIZESV(SV *sv),在某些情况下可能比过早调用 sv_2mortal() 更正确。
这些宏基本上从 Perl 5.6 或更早版本就已经存在了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接