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

问题描述 投票:43回答:7

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

选择语言

考虑到所有已知的要求和细节,我得出结论,用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++ c portability
7个回答
30
投票

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

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

  • 注意装饰和类似的“次要”修改方案,特别是如果您使用MS编译器。最值得注意的是stdcall惯例有时会导致VB的装饰生成(装饰是函数符号名后的@ 6之类的东西)
  • 并非所有编译器都可以实际布局各种结构: 所以避免过度使用工会。 避免比特包 并且最好打包32位x86的记录。虽然理论上速度较慢,但​​至少所有编译器都可以访问打包记录,随着架构的发展,官方对齐要求会随着时间的推移而发生变化
  • 在Windows上使用stdcall。这是Windows DLL的默认设置。避免快速调用,它不是完全标准化的(特别是如何传递小记录)
  • 一些使自动标题翻译更容易的提示: 由于它们的无类型,宏很难自动转换。避免它们,使用功能 为每个指针类型定义单独的类型,并且不在函数声明中使用复合类型(xtype **)。 尽可能遵循“使用前定义”口号,这将避免用户翻译标题以重新排列它们,如果他们的语言通常需要在使用前定义,并使一遍解析器更容易翻译它们。或者,如果他们需要上下文信息来自动翻译。
  • 不要暴露超过必要的。如果可能,请保留句柄类型。它只会导致版本问题。
  • 不要将结构类型(如数组的记录/结构)作为返回类型的函数返回。
  • 总是有版本检查功能(更容易区分)。
  • 小心枚举和布尔值。其他语言的假设可能略有不同。您可以使用它们,但要记录它们的行为方式和大小。还要提前考虑,并确保如果添加一些字段,枚举不会变大,打破界面。 (例如,在Delphi / pascal上默认布尔值为0或1,其他值未定义.C类布尔值有特殊类型(字节,16位或32位字大小,尽管它们最初是为COM引入的,不是C接口))
  • 我更喜欢指向char + length的字符串作为单独的字段(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,因此拥有丰富的经验。


4
投票

我不知道但是如果它适用于Windows那么你可以尝试直接的类似C的API(类似于WINAPI),或者将你的代码打包为COM组件:因为我猜想编程语言可能希望能够调用Windows API和/或使用COM对象。


3
投票

关于自动包装器生成,请考虑使用SWIG。对于Java,它将完成所有JNI工作。此外,它能够正确地转换复杂的OO-C ++接口(前提是您遵循一些基本准则,即没有嵌套类,没有过度使用模板,加上Marco van de Voort提到的那些)。


3
投票

想想C,别的什么。 C是最流行的编程语言之一。它广泛用于许多不同的软件平台,并且很少有计算机体系结构不存在C编译器。所有流行的高级语言都提供了C语言的接口。这使得您的库几乎可以从现有的所有平台访问。不要过分担心提供面向对象的界面。在C,OOP中完成库之后,可以使用适当的客户端语言创建功能或任何其他样式界面。没有其他系统编程语言可以为您提供C的灵活性和可维护性。


2
投票

我认为NestedVM会比纯Java慢,因为int [] []上的数组边界检查表示MIPS虚拟机内存。这是一个很好的概念,但现在可能表现不佳(直到手机制造商添加NestedVM支持(如果他们这样做),大多数东西现在都是缓慢的,n'est-ce pas)?虽然它可以在没有错误的情况下解压缩JPEG,但速度也不容小觑! :)

你写的东西中没有别的东西可以说出来,这并不是说它是对还是错!原则听起来(主要是听说单词和语言的选择,说实话),就像大致标准的最佳实践,但我没有仔细考虑你所说的一切细节。正如你自己所说,这真的应该是几个问题。但是当然做这种事情并不是很容易,因为你已经修复了可能与你工作过的最后一个代码库略有不同的架构......! ;)

我的想法:

您对C接口兼容性的所有评论对我来说都是合理的,除了您似乎没有正确解决内存管理策略之外,这几乎是最好的做法 - 有些句子有点含糊不清/模糊/错误听起来。内存管理的设计在很大程度上取决于应用程序中的访问模式,而不是功能本身。我想你研究别人试图制作标准ANSI C API,Unix API,Win32 API,Cocoa,J2SE等便携式接口的尝试。

如果是我,我会在常规Java和Davlik虚拟机Java的常用元素的精心选择的子集中编写库,并编写我自己的自定义解析器,将代码转换为C,用于支持C的平台,当然是大多数人。我建议如果你限制自己使用各种大小的int,bools,字符串,字典和数组的数据类型,并仔细使用它们,这将有助于跨平台问题,而不会在大多数情况下影响性能。


2
投票

你的假设似乎没问题,但我看到了前方的麻烦,你已经在你的假设中发现了很多。正如你所说,你无法真正导出c ++类和方法,你需要提供一个基于函数的c接口。无论你构建什么外观,它都将是一个基于功能的界面。

我看到的基本问题是人们选择特定语言及其运行时因为他们的思维方式(功能或面向对象)或他们解决的问题(网络编程,数据库......)以某种方式对应于该语言或其他。在c中实现的库可能永远不会像他们习惯使用的库那样,除非他们自己编程。就个人而言,当我使用python时,我总是更喜欢“感觉像python”的库,而当我使用Java EE时,我总是喜欢感觉像java的库,即使我知道c和c ++。

因此,您的努力可能几乎没有实际用途(除了您获得的经验),因为人们可能希望坚持他们的思维方式,而是重新实现功能,而不是使用完成工作但不适合的库。

我也担心理想的可移植性会严重妨碍开发。只需考虑所需的无限构建设置,并对其进行测试。我曾经研究过一个试图保持5个操作系统兼容性的项目(所有类似posix,但仍然如此)和大约10个编译器,这些构建是测试和维护的噩梦。


0
投票

给它一个XML接口,无论是作为参数传递还是返回值,还是通过命令行调用传递给文件。这可能看起来不像普通的函数接口那样直接,但是从例如Java访问可执行文件的最实用的方法。

© www.soinside.com 2019 - 2024. All rights reserved.