Haskell FFI中"wrapper"包装器的实现

5
根据 Haskell Wiki 的说法,在 Haskell FFI 中,我们可以使用“wrapper”包装器将一个 Haskell 函数转换为 C 函数。
foreign import ccall "wrapper" createFunPtr :: (Int -> Int) -> IO (FunPtr (Int -> Int))

如果我理解正确,这表示在do块中f <- createFunPtr (+ 42)会给出一个C语言类型为int (*)(int)的函数指针f

在Haskell中,类型为Int -> Int的函数可能具有一些内部绑定(例如,lambda表达式可能引用外部作用域中的变量),而在C中,函数指针仅仅是指向函数的内存地址,调用这些函数指针只是类似于原始跳转的东西。因此,Haskell函数的其他数据无处可存储FunPtr

C++中的lambda表达式是对象,调用operator()会传递一个隐式的this指针。但是,FunPtr在C中被视为普通函数指针,因此没有可能传递一些额外的参数。

那么,GHC是如何实现这个“包装器”的包装器的呢?(我猜它可能是通过直接将指令写入内存中的代码段来传递额外的参数来实现的,但是据我所记,代码段通常是只读的。)


在标准 C 中这是不可能的,但在许多操作系统中有办法在运行时生成代码并获得有效的函数指针。这大致是动态库的工作原理。可能 GHC 生成的机器码包含对捕获变量的硬编码指针。此外,这里有一个丑陋的概念验证 - chi
我猜它会为无状态函数创建直接调用指针,对于闭包,则要么在运行时动态生成它们(这是可能的,你可以在正在运行的程序中为代码创建新的缓冲区——这就是 JIT 的工作原理),要么预先计算它们的类型并使用某种跳板来跳转到特定的闭包。 - Bartek Banachewicz
1个回答

5
快速谷歌搜索会出现GHC评论
偶尔,将Haskell闭包视为C函数指针是很方便的。例如,如果我们想在现有的C库中安装Haskell回调,则这非常有用。借助调整器thunk实现了此功能。
调整器thunk是一种动态分配的代码片段,允许将Haskell闭包视为C函数指针。
稳定指针提供了一种方法,使外部世界可以访问并评估Haskell堆对象,并且运行时系统提供了一小组操作来实现这一点。因此,假设我们在C中手头有一个稳定指针,我们可以跳转到Haskell世界并评估回调过程。在某些使用回调的情况下,这可以正常工作,但确实需要外部代码了解稳定指针及其处理方式。我们希望隐藏回调的Haskell特性,并像任何其他C函数指针一样调用它。
进入调整器thunk。调整器thunk是即时生成的小段代码(每个导出的Haskell闭包一个),当使用某些“通用”调用约定(例如,在平台X上的C调用约定)输入时,在调用另一个(静态)C函数存根之前,将推送一个隐式稳定指针(到Haskell回调)。该存根负责通过其稳定指针进入Haskell代码。
调整器thunk在C堆上分配,并在分配给Haskell(IO)操作的函数指针之前,在Haskell内部调用。用户代码不应明确调用它。
调整器thunk与C函数指针在一个方面不同:当代码完成时,必须释放它以释放Haskell和C资源。否则,将导致C和Haskell两侧的内存泄漏。
我记得在某个地方读到过,包装器FFI导入实际上是GHC执行运行时代码生成的唯一位置。
我认为评论所说的是,您的createFunPtr在编译时被定义为类似于这样的东西(我设置了-ddump-simpl以获取createFunPtr的核心代码,并且以下是我的尝试将其反汇编回Haskell)。
createFunPtr fun = do stable <- newStablePtr fun
                      pkg_ccall stable :: IO (FunPtr (Int -> Int))

newStablePtrStablePtr API 的一部分,它允许 Haskell 将对 Haskell 对象的引用导出到外部代码。GC 允许在创建 adjustor thunk 后移动传递给 createFunPtr 函数的函数。因此,该 adjustor 需要一个引用到仍然在 GC 后保持的函数,并且 stable pointers 提供了该功能。

pkg_ccall(实际上相当神奇)在C堆上分配调整器thunk的空间。这个空间必须稍后使用freeHaskellFunPtr释放,否则会在C堆(保存调整器)和Haskell堆(保存函数闭包,直到稳定指针被释放之前无法进行垃圾回收)上发生内存泄漏。调整器的内容取决于平台以及GHC是否在构建时配置为使用libffi进行调整器。实际的汇编代码可以在相关RTS文件的注释中找到,但要点通常是:

int adjustor(int arg) {
  return zdmainzdTzdTzucreateAddPtr($stable, arg);
  // with stable "baked" into each adjustor, as a "push <constant>" instruction
}

zdmainzdTzdTzucreateAddPtr是一个存根,它会取消引用给定的稳定指针并调用其中生成的Haskell函数。它是静态的,嵌入到二进制文件中,并且与此类似:(如果您传递GHC -v-keep-tmp-files,则应该能够找到包含真实定义的ghc_<some_num>.c文件,该文件需要进行一些簿记。)

HsInt zdmainzdTzdTzucreateAddPtr(StgStablePtr ptr, HsInt a) {
  HaskellObj fun, arg, app, ret;
  fun = deRefStablePtr(ptr);
  arg = rts_mkInt(a);
  app = rts_apply(fun, arg);
  eval(app, &ret);
  return rts_getInt(ret);
}

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