使用安全的用户界面将Crystal回调传递给C结构

前言

Crystal允许我们编写到C库的绑定,因此我们无需重新发明轮子即可使用功能强大的现有解决方案。 但是有时,尤其是作为C新手,很难弄清楚如何在某些C构造之上进行绑定和构建。 此问题通常分为以下步骤:

  1. 阅读该库的文档,标题和源文件,以了解将要绑定的内容。
  2. 编写Crystal lib定义,公开用户需要的funstructenum等。
  3. 用一个高级API封装该lib ,使您的用户感到熟悉,安全和强大–最好的情况是,它是C绑定,这实际上只是实现细节,您的用户甚至无需考虑!

在本文中,我将分享我与C库绑定的经验,该过程希望用户传递C函数指针的C结构,这些C函数用作从C库中其他例程运行的回调。 在此之前,我不知道有关如何执行C绑定的最新文档,因此希望这对其他人也有帮助!

我将尽力在此过程中解释重要的核心概念,但是将假定您对C,指针,类型和使用C编译器有一定的了解或了解。 哦,还有水晶。 如果您不确定,建议快速阅读有关这些概念的其他一些教程。

C

接口

准备阅读一些C代码吗? 这是我们将绑定到的库:

让我们回顾一下。

#include

这是一个标准的C标头,可让我们使用有用的常量,例如NULL 。 对于我们的用例,我们无需担心。

struct operations

这是将保存我们的回调的C结构。 那里有两个:

  • void (*work) (int *, int, int); 一个函数指针,它接受对一个整数和两个整数的引用。 预期存储在此成员中的函数会对这两个整数做一些工作,并将结果存储在引用中。
  • void (*log) (int); 接受整数的函数指针。 只需使用work回调完成的结果副本调用存储在该成员中的函数。 在我们的例子中,我们将使用它来实现一些到STDOUT日志记录。

void call(const struct operations *ops, int a, int b)

这是库提供的主要功能。 它接受对一组回调(我们的operations结构)的引用,以及两个在这些回调之间分配的整数。

执行回调的行之前是NULL检查。 这使得每个回调都是可选的,因此,如果我们在代码中未定义log处理程序,则C库将不执行任何操作。 如果不存在此检查,它将尝试访问无效的内存地址; kaboom,segfault!

而已。 实际上,在将回调操作传递给其他地方之后, call将从其他一些C代码运行。 为了我们的目的,我们将从Crystal绑定中运行call ,以使其更易于使用。

示例C的用法

这是在C语言中实现使用上述库的演示。对于本文的其余部分,假定test.c是先前审阅过的C库。 这主要是出于完整性的考虑,对于已经拥有C语言背景的人们来说,这可能会使我们所说的更加具体:

水晶

库定义

如果您正在编码,请确保已将我们的库编译为test.o文件! 另外,如果您对C库进行了更改,请不要忘记重新编译它。 在运行我们的Crystal程序时,它将很高兴使用过期的目标文件!

我们将从编写我们的lib定义开始:

@[Link(ldflags: "#{__DIR__}/test.o")]

这只是告诉Crystal编译器我们正在链接到当前目录中的test.o 在实践中,您可能永远不需要使用这样的ldflags ,因为您要绑定的库可能已经在系统PATH可用,因此只需@[Link("foo")]就足够了。

struct Operations

这是我们对operations C结构的Crystal表示。 最终结果很简单,但是我花了一些时间才能真正做到这一点。

首先,我开始编写此结构,以与在纯Crystal中完成的方式相同的方式对其进行描述:

Proc通常是我们如何表示和处理Crystal空间中用户的回调。 但是,通过一些研究,我发现了为什么这实际上是不正确的:

  1. Crystal中C struct中成员的类型必须与C中struct在内存中的实际表示方式匹配。 尽管C定义确实给出了参数说明,但这不是成员类型的一部分。 在这两种情况下,我们都在处理函数指针,这意味着这两个字段都必须为Void*
  2. 当您编写libstructs实际上并未“绑定”到相应的C结构! 晶体结构与C兼容 。 这意味着您可以为Crystal struct命名任何东西,为它提供任意数量的任何类型的成员,并且直到运行时您都不会发现它的错误。 小心!

fun call(ops : Operations*, a : LibC::Int, b : LibC::Int)

这是我们对call函数的绑定,将用于测试来自Crystal代码的回调。 这里没什么特别的。

库使用示例

现在我们有了C lib绑定,这已经可以使用了! 让我们写一个简单的例子来测试它是否有效。 让我们从之前复制C示例:

有用!

您会注意到,我们必须在回调中直接使用一个指针: result : Int32* 。 这是我们在编写包装器时要考虑的事情。

我们如何避免将此指针暴露给用户,并保证它接收到正确类型的值?

安全包装

我们可以立即将此库包装到更高级别的类中,以立即获得一些好处:

并以类似方式使用它:

看起来开始变得更加友好了! 这是我们现在的位置:

  • 熟悉的Crystal语法和语义。 您正在与C库进行交互几乎完全被遮盖了。
  • 编译器强制用户传递正确的回调类型。 我们可以安然入睡,知道用户回调是合理的,并且应该可以在我们的C库中按预期工作。
  • 但是,我们仍然坚持将Pointer暴露给用户。

接下来,我们将探索隐藏该指针,以及为使该API安全而需要进行的重构。

问题:关闭

理想的界面是什么样的? 大概是这样的:

听起来不错。 现在,指针已从我们的公共界面中消失,用户可以插入自己的行为。 类型限制保证了我们将返回一个Int32 ,因此我们可以安全地将其分配给指针。

但是,我们遇到了麻烦:

不好! 发生了什么? 让我们退后一步,讨论一下Proc

考虑以下代码:

运行该程序将打印如下内容:

这说明了一些事情:

  • ab都是Proc(Int32) 。 但是b被标记为closure 。 只要在proc的主体内部引用了proc本地范围之外的内容,它就“神奇地”被标记为闭包。
  • 这两种类型的大小均为16个字节。 这是因为Crystal Proc在内部由两部分组成– 8个字节是指向proc主体的指针,另外8个字节是指闭包的上下文,即闭包的数据。 通过调用proc.pointerproc.closure_data将proc细分为各个部分。
  • C没有意识到这些额外的数据; 它仅接收8个字节的指针,该指针在被调用时将尝试访问0x0NULL )处的无效内存,并进行段错误。

第一个要点有一个例外,它为我们的问题提供了解决方案。 proc本地范围之外的静态构造不会将其标记为关闭。 这是因为这些构造的位置在内存中永远不会改变,而诸如局部变量,实例变量等之类的东西将位于不同的位置,并且可以随着堆栈的变化而移动。

解决方案:静态方法

我们已经确定我们不能接受来自用户的阻止,因为这将创建一个闭包,并且我们需要某种方式来接受来自用户的静态接口

类方法提供了以下确切信息:

大! 我们还可以使用自由变量将此接口(用户将定义)传递给我们的Test API。 让我们开始重写我们的课程:

现在我们可以像这样使用它:

这里有很多事情,所以让我们来看一些内容:

def initialize(interface : T.class) forall T

这是我们的新构造函数,它接受任何元类T并允许我们稍后在我们的方法中对其进行引用。 当编译器构造我们的Test.new(MyOps)实例时,在编译时将T替换为MyOps内部的MyOps 。 这意味着,如果MyOps的接口被用户错误定义或被我们的实现误用,我们将得到编译时错误而不是运行时错误。

例如,如果MyOps.handle_work返回一个String ,则它将无法编译,因为result.valuePointer(Int32)#value=(obj) result.value Pointer(Int32)#value=(obj) )无法接收它!

另外,如果我们仅编写def self.handle_work(a) ,则由于参数数量错误,我们将收到编译时错误。

{% if T.class.has_method?(:handle_work) %}

如果您还记得,我们的C库的回调是可选的。 因此,如果我们的Test类的用户不想实现特定的回调,则此宏检查允许他们在其界面中将其保留为未定义状态。 如果我们没有进行此项检查,则编译器将引发一个未定义的方法。 如果C库要为我们提供默认回调,则这是捕获的重要语义。

raise "work f is closure" if f.closure?

据我所知,这是一个健全性检查,很遗憾在编译时无法完成。 我将其保留为一种额外的安全措施,以防万一Test被黑客入侵并且回调以某种方式变为关闭,或者在Crystal的类型系统中还缺少其他边缘情况。

最后的例子

综上所述,我们现在有了一种非常安全的方法,允许用户将自己的实现应用于这些C函数。 这是一个示例,为Test应用创建了一些不同的实现:

输出:

闭幕

我希望这是一篇有关在Crystal中编写与C代码的绑定的知识性读物,以及如何借助Crystal编译器使这些C库的应用程序安全。

我绝不是C或编程语言方面的专家,因此,如果我在任何地方混淆了我的文字,或者说不清楚的地方,请发表评论! 我很乐意修复它。

如果您想取得联系,我在Gitter / IRC以及Crystal社区论坛中为@ z64。

也可以在GitHub上查看我的项目:

https://github.com/z64

感谢您的阅读! ❤