
前言
Crystal允许我们编写到C库的绑定,因此我们无需重新发明轮子即可使用功能强大的现有解决方案。 但是有时,尤其是作为C新手,很难弄清楚如何在某些C构造之上进行绑定和构建。 此问题通常分为以下步骤:
- 阅读该库的文档,标题和源文件,以了解将要绑定的内容。
- 编写Crystal
lib
定义,公开用户需要的fun
,struct
,enum
等。 - 用一个高级API封装该
lib
,使您的用户感到熟悉,安全和强大–最好的情况是,它是C绑定,这实际上只是实现细节,您的用户甚至无需考虑!
在本文中,我将分享我与C库绑定的经验,该过程希望用户传递C函数指针的C结构,这些C函数用作从C库中其他例程运行的回调。 在此之前,我不知道有关如何执行C绑定的最新文档,因此希望这对其他人也有帮助!
我将尽力在此过程中解释重要的核心概念,但是将假定您对C,指针,类型和使用C编译器有一定的了解或了解。 哦,还有水晶。 如果您不确定,建议快速阅读有关这些概念的其他一些教程。
C
接口
准备阅读一些C代码吗? 这是我们将绑定到的库:
- 现实世界中的面向对象编程示例
- “羞耻驱动的开发”-什么时候以及如何让Boting比阻止更好
- 低代码和抽象级别
- 3 Hal Mengapa编程Mengubah'Mindset'Pola Pikir。
- 内部创业公司:一名技术专家
让我们回顾一下。
#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空间中用户的回调。 但是,通过一些研究,我发现了为什么这实际上是不正确的:
- Crystal中C
struct
中成员的类型必须与C中struct
在内存中的实际表示方式匹配。 尽管C定义确实给出了参数说明,但这不是成员类型的一部分。 在这两种情况下,我们都在处理函数指针,这意味着这两个字段都必须为Void*
。 - 当您编写
lib
,structs
实际上并未“绑定”到相应的C结构! 晶体结构与C兼容 。 这意味着您可以为Crystalstruct
命名任何东西,为它提供任意数量的任何类型的成员,并且直到运行时您都不会发现它的错误。 小心!
fun call(ops : Operations*, a : LibC::Int, b : LibC::Int)
这是我们对call
函数的绑定,将用于测试来自Crystal代码的回调。 这里没什么特别的。
库使用示例
现在我们有了C lib绑定,这已经可以使用了! 让我们写一个简单的例子来测试它是否有效。 让我们从之前复制C示例:
有用!
您会注意到,我们必须在回调中直接使用一个指针: result : Int32*
。 这是我们在编写包装器时要考虑的事情。
我们如何避免将此指针暴露给用户,并保证它接收到正确类型的值?
安全包装
我们可以立即将此库包装到更高级别的类中,以立即获得一些好处:
并以类似方式使用它:
看起来开始变得更加友好了! 这是我们现在的位置:
- 熟悉的Crystal语法和语义。 您正在与C库进行交互几乎完全被遮盖了。
- 编译器强制用户传递正确的回调类型。 我们可以安然入睡,知道用户回调是合理的,并且应该可以在我们的C库中按预期工作。
- 但是,我们仍然坚持将
Pointer
暴露给用户。
接下来,我们将探索隐藏该指针,以及为使该API安全而需要进行的重构。
问题:关闭
理想的界面是什么样的? 大概是这样的:
听起来不错。 现在,指针已从我们的公共界面中消失,用户可以插入自己的行为。 类型限制保证了我们将返回一个Int32
,因此我们可以安全地将其分配给指针。
但是,我们遇到了麻烦:
不好! 发生了什么? 让我们退后一步,讨论一下Proc
。
考虑以下代码:
运行该程序将打印如下内容:
这说明了一些事情:
-
a
和b
都是Proc(Int32)
。 但是b
被标记为closure
。 只要在proc的主体内部引用了proc本地范围之外的内容,它就“神奇地”被标记为闭包。 - 这两种类型的大小均为16个字节。 这是因为Crystal
Proc
在内部由两部分组成– 8个字节是指向proc主体的指针,另外8个字节是指闭包的上下文,即闭包的数据。 通过调用proc.pointer
和proc.closure_data
将proc细分为各个部分。 - C没有意识到这些额外的数据; 它仅接收8个字节的指针,该指针在被调用时将尝试访问
0x0
(NULL
)处的无效内存,并进行段错误。
第一个要点有一个例外,它为我们的问题提供了解决方案。 proc本地范围之外的静态构造不会将其标记为关闭。 这是因为这些构造的位置在内存中永远不会改变,而诸如局部变量,实例变量等之类的东西将位于不同的位置,并且可以随着堆栈的变化而移动。
解决方案:静态方法
我们已经确定我们不能接受来自用户的阻止,因为这将创建一个闭包,并且我们需要某种方式来接受来自用户的静态接口 。
类方法提供了以下确切信息:
大! 我们还可以使用自由变量将此接口(用户将定义)传递给我们的Test
API。 让我们开始重写我们的课程:
现在我们可以像这样使用它:
这里有很多事情,所以让我们来看一些内容:
def initialize(interface : T.class) forall T
这是我们的新构造函数,它接受任何元类T
并允许我们稍后在我们的方法中对其进行引用。 当编译器构造我们的Test.new(MyOps)
实例时,在编译时将T
替换为MyOps
内部的MyOps
。 这意味着,如果MyOps
的接口被用户错误定义或被我们的实现误用,我们将得到编译时错误而不是运行时错误。
例如,如果MyOps.handle_work
返回一个String
,则它将无法编译,因为result.value
( Pointer(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
感谢您的阅读! ❤