分享

如何设计一个可以在许多客户端语言中使用的C/C 库?

 印度阿三17 2019-10-03

我打算编写一个应该可以被广泛的平台上的大量人使用的库.我需要考虑什么来设计它?为了使这个问题更具体,最后有四个“子问题”.

选择语言

考虑到所有已知的要求和细节,我得出结论,用C或C编写的库是可行的方法.我认为我的库的主要用途是在用C,C和Java SE编写的程序中,但我也可以考虑从Java ME,PHP,.NET,Objective C,Python,Ruby,bash scrips中使用它的原因,等等……也许我无法针对所有这些,但如果有可能,我会做到的.

要求

在这里描述我的库的全部目的将是很多,但有些方面可能对这个问题很重要:

>图书馆本身起初很小,但肯定会变得极其复杂,因此不能同时维护多个版本.
>但是,大多数复杂性将隐藏在库中
>该库将构建一个在内部大量使用的对象图.库的某些客户端只对特定对象的特定属性感兴趣,而其他客户端必须以某种方式遍历对象图
>客户端可能会更改对象,并且必须通知库
>库可能会更改对象,如果客户端已有该对象的句柄,则必须通知客户端
>库必须是多线程的,因为它将保持与其他几个主机的网络连接
>虽然可以同步处理对库的一些请求,但其中许多请求将花费太长时间并且必须在后台处理,并在成功(或失败)时通知客户端

当然,无论答案是满足我的具体要求,还是以一般方式回答问题,对更广泛的受众都有帮助,我们都欢迎回答!

我的假设,到目前为止

以下是我在过去几个月收集的一些假设和结论:

>在内部,我可以使用我想要的任何东西,例如C带有运算符重载,多重继承,模板元编程……只要有一个处理它的可移植编译器(想想gcc / g)
>但我的界面必须是一个干净的C接口,不涉及名称修改
>另外,我认为我的界面应该只包含函数,基本/原始数据类型(可能是指针)作为参数传递并返回值
>如果我使用指针,我想我应该只使用它们将它们传递回库,而不是直接在引用的内存上操作
>为了在C应用程序中使用,我可能还提供面向对象的接口(也容易出现名称错误,因此App必须使用相同的编译器,或者以源代码形式包含库)
>在C#中使用也是如此吗?
>在Java SE / Java EE中使用时,Java本机接口(JNI)适用.我有一些关于它的基本知识,但我绝对应该仔细检查它.
>并非所有客户端语言都能很好地处理多线程,因此应该有一个线程与客户端通信
>对于Java ME的使用,没有JNI这样的东西,但我可能会使用Nested VM
>要在Bash脚本中使用,必须有一个带命令行界面的可执行文件
>对于其他客户端语言,我不知道
>对于大多数客户端语言,使用该语言编写适配器接口会很好.我认为有一些工具可以自动为Java和其他一些人生成这个
>对于面向对象的语言,有可能创建一个面向对象的适配器,它隐藏了库的接口是基于函数的这一事实 – 但我不知道它是否值得付出努力

可能的子问题

>这是可行的,可管理的努力,还是只是太多的便携性?
>有关于此类设计标准的好书/网站吗?
>我的任何假设是错误的吗?
>哪些开源库值得学习从他们的设计/界面/源学习?
> meta:这个问题相当长,你有没有办法将它分成几个较小的? (如果您回复此问题,请将其作为评论,而不是作为答案)

解决方法:

大多是正确的.直接的程序界面是最好的. (这与C btw(**)不完全相同,但足够接近)

我接口DLL很多(*),包括开源和商业,所以这里有一些我记得日常练习的要点,请注意这些是更值得研究的推荐区域,而不是基本的事实:

>注意装饰和类似的“次要”修改方案,特别是如果您使用MS编译器.最值得注意的是stdcall惯例有时会导致VB的装饰生成(装饰是函数符号名后的@ 6之类的东西)
>并非所有编译器都可以实际布局各种结构:

>所以避免过度使用工会.
>避免比特包
>并且最好打包32位x86的记录.虽然理论上速度较慢,但至少所有编译器都可以访问打包记录,随着架构的发展,官方对齐要求会随着时间的推移而发生变化

>在Windows上使用stdcall.这是Windows DLL的默认设置.避免快速调用,它不是完全标准化的(特别是如何传递小记录)
>一些使自动标题翻译更容易的提示:

>由于它们的无类型,宏很难自动转换.避免它们,使用功能
>为每个指针类型定义单独的类型,并且不要在函数声明中使用复合类型(xtype **).
>尽可能遵循“使用前定义”口头禅,这将避免用户翻译标题以重新排列它们,如果他们的语言通常需要在使用前定义,并使一遍解析器更容易翻译它们.或者,如果他们需要上下文信息来自动翻译.

>不要暴露超过必要的.如果可能,请保留句柄类型.它只会导致版本问题.
>不要将记录/结构或数组等结构化类型作为函数的返回类型返回.
>始终具有版本检查功能(更容易区分).
>小心枚举和布尔值.其他语言的假设可能略有不同.您可以使用它们,但要记录它们的行为方式和大小.还要提前考虑,并确保如果添加一些字段,枚举不会变大,打破界面. (例如,在Delphi / pascal上默认布尔值为0或1,其他值未定义.C类布尔值有特殊类型(字节,16位或32位字大小,尽管它们最初是为COM引入的,不是C接口))
>我更喜欢指向char长度的字符串作为单独的字段(COM也这样做).优选地,不必依赖零终止.这不仅仅是因为安全(溢出)原因,还因为以这种方式将它们与Delphi本机类型连接更容易/更便宜.
>内存始终以鼓励完全分离内存管理的方式创建API. IOW不假设有关内存管理的任何内容.这意味着lib中的所有结构都是通过您自己的内存管理器分配的,如果函数将结构传递给您,则复制它而不是存储使用“clients”内存管理创建的指针.因为你迟早会意外地免费或重新分配它:-)
>(实现语言,不是接口),不愿意更改协处理器异常掩码.某些语言将其更改为符合其标准浮点错误(异常)处理的一部分.
>始终将回调与用户可配置的上下文配对.用户可以使用它来提供回调状态而不定义全局变量. (例如对象实例)
>小心处理协处理器状态字.它可能会被其他人更改并破坏您的代码,如果您更改它,其他代码可能会停止工作.状态字通常不作为调用约定的一部分保存/恢复.至少在实践中没有.
>不要使用C风格的varargs参数.并非所有语言都以不安全的方式允许可变数量的参数
(*)Delphi程序员白天,这项工作涉及连接大量硬件,从而转换供应商SDK头.到了晚上Free Pascal开发人员,负责Windows头文件.

(**)
这是因为“C”意味着二进制仍然依赖于使用的C编译器,特别是如果没有真正的通用系统ABI.想想像这样的东西:

> C在某些二进制格式上添加下划线前缀(a.out,Coff?)
>有时不同的C编译器对如何处理按值传递的小结构有不同的看法.按照官方的说法,他们不应该支持它,但大多数人都这样做.
>结构打包有时会有所不同,调用约定的细节也有所不同(如跳过
 如果参数可在FPU寄存器中注册,则是否为整数寄存器)

=====自动标头转换====

虽然我不太了解SWIG,但我知道并使用了一些特定于delphi的头文件工具(h2pas,Darth / headconv等).

但是我从不在全自动模式下使用它们,因为更常见的是输出很少.注释更改行或被剥离,并且不保留格式.

我通常制作一个小脚本(在Pascal中,但你可以使用任何具有良好字符串支持的东西)将头部分开,然后在相对同质的部分上尝试工具(例如,只有结构,或者只定义等).

然后我检查我是否喜欢自动转换输出,并使用它,或者尝试自己制作一个特定的转换器.由于它适用于子集(仅限于结构),因此通常比制作完整的标头转换器更容易.当然,这取决于我的目标是什么. (漂亮,可读的标题或快速和脏).在每一步,我可能会做一些替换(使用sed或编辑器).

我为Winapi commctrl和ActiveX / comctl标头做的最复杂的方案.在那里我结合了IDL和C头(接口的IDL,C中的一堆不可解析的宏,其余的C头),并设法获得大约80%的宏类型(通过传播sendmessage中的类型转换)宏返回宏声明,有合理的(wparam,lparam,lresult)默认值)

半自动方式的缺点是声明的顺序不同(例如,第一个常量,然后是结构,然后是函数声明),这有时会使维护变得困难.因此,我始终保持原始标题/ sdk与之比较.

Jedi winapi转换项目可能有更多的信息,他们将大约一半的Windows标题翻译成Delphi,因此拥有丰富的经验.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多