unsafePerformIO和FFI库初始化

13

我正在创建一个FFI模块,连接到一个在C中的库,该库要求调用一个一次性、不可重入的函数,在任何其他操作之前。这个调用是幂等的,但具有状态,因此我可以在每个Haskell调用中都调用它。但是这样做很慢,并且由于不可重入性可能会导致冲突。

那么现在是使用unsafePerformIO的正确时间吗?我可以在一个不安全的IORef或MVar中包装一个布尔值,通过忽略后续调用(全局隐藏的IORef状态为False的调用)使这些初始化调用变得幂等。

如果不是,那么正确的方法是什么?

2个回答

11

我更喜欢初始化一次并提供一个无法伪造的令牌作为证明你已经初始化了机器的方法。

因此,您的证据将是:

data Token = Token

抽象地导出的内容。

然后,您的初始化函数可以返回此证据。

init :: IO Token
现在,您需要将该证明传递给您的API:
bar  :: Token -> IO Int
bar !tok = c_call_bar

等等,你可以使用单子或某些高阶初始化环境来封装这个东西,使其更加简洁,但这就是基本思路。

使用隐藏状态初始化C库的问题在于,要么不能并行访问库,要么在GHCi中出现问题,混合编译和字节码,并加载两个不同版本的C库(这将导致链接器错误)。


2
一个已经被使用的替代方案是在主函数周围使用 withX 包装器。这并不提供静态保证,我只是说有先例(例如来自网络包的 withSocketsDo)。 - Thomas M. DuBuisson
1
啊,好的观点。比“withToken $ \t ->”更简单,但不能保证。 - Don Stewart
1
啊,太好了!这是一个更好的解决方案。我一直担心全局状态会如何与多线程交互(MVar是否不安全且线程本地化,运行时本地化?)。这也使得Haskell运行时中的初始化失败可以被定位,而不仅仅是隐含和隐藏的。 - J. Abrahamson
1
我一直在思考是否要“用单子来包装这些东西”。从一方面来看,这个概念上很好:在新的单子中,你有这种特殊单子类型的操作,只有在初始化后才能正确调用。为了将它们与普通IO操作交错,我必须为此创建一个单子变换器(并提升IO操作)。但是我想知道:这真的值得吗?!人们可能希望使用单子来排序操作(不同的修改初始化状态的顺序会产生不同的结果),但IO已经强制排序。它消除了智能单子的需求! - imz -- Ivan Zakharyaschev
1
关于为依赖于幂等(有状态)先前操作的操作创建单子(“现在用一个单子把这些东西包起来”):我刚刚阅读了一个可以用于更一般问题的想法:输出许多createDirectory,然后使用nub减少此操作列表并将其馈送到IO中。如果只有一个未参数化的要初始化的事物,则可以避免nub的开销。 - imz -- Ivan Zakharyaschev
显示剩余2条评论

2

我想指出,目前有一些新的技巧被Neil Mitchell建议用于替代withSocketsDo这是基于evaluate(“强制执行其参数到弱头正常形式,当执行产生IO操作时。”):

withSocketsDo act = do evaluate withSocketsInit; act 

{-# NOINLINE withSocketsInit #-}
withSocketsInit = unsafePerformIO $ do
    initWinsock
    termWinsock

我去除调用withSocketsDo的方法是让它变得非常便宜,然后在可能需要的地方随处使用。

这并不一定是一个好主意...

(另请参见他在库中宣布此更新的答案。)


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