为面向对象的C++代码开发C封装器API

97

我希望开发一组 C API,封装我们现有的 C++ API 以访问我们的核心逻辑(使用面向对象的 C++ 编写)。这将是一个粘合 API,允许其他语言使用我们的 C++ 逻辑。有哪些好的教程、书籍或最佳实践介绍将 C 封装在面向对象的 C++ 环境中的概念?


5
参考zeromq源代码以获得灵感。该库目前是用C++编写的,并具有C绑定。http://www.zeromq.org/ - Hassan Syed
1
相关(甚至是重复的):将C++类API封装为C可用 - user
6个回答

83

这并不太难手动完成,但取决于您接口的大小。我遇到过的情况是为了使我们的C++库能够在纯C代码中使用,因此SWIG没有什么帮助。(也许SWIG可以用来做到这一点,但我不是SWIG大师,而且它似乎不容易)

我们最终所做的就是:

  1. 每个对象在C中都通过一个不透明句柄传递。
  2. 构造函数和析构函数被包装在纯函数中。
  3. 成员函数是纯函数。
  4. 其他内置函数尽可能映射到C等效函数。

因此,像这样的类(C++头文件)

class MyClass
{
  public:
  explicit MyClass( std::string & s );
  ~MyClass();
  int doSomething( int j );
}

对应的C接口可能会像这样(C头文件):

struct HMyClass; // An opaque type that we'll use as a handle
typedef struct HMyClass HMyClass;
HMyClass * myStruct_create( const char * s );
void myStruct_destroy( HMyClass * v );
int myStruct_doSomething( HMyClass * v, int i );

接口的实现看起来像这样(C++源代码)

#include "MyClass.h"

extern "C" 
{
  HMyClass * myStruct_create( const char * s )
  {
    return reinterpret_cast<HMyClass*>( new MyClass( s ) );
  }
  void myStruct_destroy( HMyClass * v )
  {
    delete reinterpret_cast<MyClass*>(v);
  }
  int myStruct_doSomething( HMyClass * v, int i )
  {
    return reinterpret_cast<MyClass*>(v)->doSomething(i);
  }
}
我们从原始类中获取不透明句柄以避免需要任何类型转换,但这似乎在我的当前编译器上不起作用。我们必须将句柄设为结构体,因为C不支持类。这为我们提供了基本的C接口。如果您想要一个更完整的示例,演示一种集成异常处理的方法,则可以在我的github上尝试我的代码:https://gist.github.com/mikeando/5394166。有趣的部分是现在确保您正确地将所有所需的C++库链接到您的大型库中。对于gcc(或clang),这意味着只需使用g++进行最终链接阶段即可。

11
我建议你使用与void不同的东西,例如匿名结构体代替void*作为返回的对象。这样可以为返回的句柄提供某种类型安全性。请访问https://dev59.com/9nRA5IYBdhLWcg3wwwvD了解更多信息。 - Laserallan
3
我同意Laserallan的观点,并相应地重构了我的代码。 - Michael Anderson
3
在extern "C"块中使用new和delete是可以的。extern "C"只会影响名称修饰,C编译器永远不会看到该文件,只会看到头文件。 - Michael Anderson
2
我还错过了一个在C中使所有代码编译所需的typedef。奇怪的typedef struct Foo Foo; "hack"。代码已更新。 - Michael Anderson
5
@MichaelAnderson,你的myStruct_destroymyStruct_doSomething函数中有两个错别字。应该是reinterpret_cast<MyClass*>(v) - firegurafiku
显示剩余18条评论

19

我认为Michael Anderson的回答是正确的,但我的方法会有所不同。你必须关注一个额外的问题:异常。异常并不是C ABI的一部分,因此你不能让异常在C++代码之外抛出。因此你的头文件将如下所示:

#ifdef __cplusplus
extern "C"
{
#endif
    void * myStruct_create( const char * s );
    void myStruct_destroy( void * v );
    int myStruct_doSomething( void * v, int i );
#ifdef __cplusplus
}
#endif

你的包装类的 .cpp 文件将如下所示:

void * myStruct_create( const char * s ) {
    MyStruct * ms = NULL;
    try { /* The constructor for std::string may throw */
        ms = new MyStruct(s);
    } catch (...) {}
    return static_cast<void*>( ms );
}

void myStruct_destroy( void * v ) {
    MyStruct * ms = static_cast<MyStruct*>(v);
    delete ms;
}

int myStruct_doSomething( void * v, int i ) {
    MyStruct * ms = static_cast<MyStruct*>(v);
    int ret_value = -1; /* Assuming that a negative value means error */
    try {
        ret_value = ms->doSomething(i);
    } catch (...) {}
    return ret_value;
}

更好的方法是:如果你知道你只需要一个MyStruct的单个实例,不要冒险处理传递给你API的void指针。而是像这样做:

static MyStruct * _ms = NULL;

int myStruct_create( const char * s ) {
    int ret_value = -1; /* error */
    try { /* The constructor for std::string may throw */
        _ms = new MyStruct(s);
        ret_value = 0; /* success */
    } catch (...) {}
    return ret_value;
}

void myStruct_destroy() {
    if (_ms != NULL) {
        delete _ms;
    }
}

int myStruct_doSomething( int i ) {
    int ret_value = -1; /* Assuming that a negative value means error */
    if (_ms != NULL) {
        try {
            ret_value = _ms->doSomething(i);
        } catch (...) {}
    }
    return ret_value;
}

这个 API 更加安全。

但是,正如 Michael 提到的那样,链接可能会变得非常棘手。

希望这可以帮到你。


2
关于此情况的异常处理更多信息,请查看以下线程: https://dev59.com/RHRA5IYBdhLWcg3wvQhh - Laserallan
2
当我知道我的C++库也将有一个C API时,我会在我的异常基类中封装一个API错误代码int。这样,在抛出异常的地方更容易知道确切的错误条件并提供非常具体的错误代码。外部C API函数中的try-catch“包装器”只需要检索错误代码并将其返回给调用者。对于其他标准库异常,请参考Laserallan的链接。 - Emile Cormier
2
catch(...){ }是纯粹的邪恶。我唯一的遗憾是我只能点一次踩。 - Terry Mahaffey
2
@Terry Mahaffey,我完全同意你的看法,这是一种邪恶的做法。最好的方法是按照Emile所建议的去做。但是,如果你必须保证包装的代码永远不会抛出异常,那么你别无选择,只能在所有其他已识别的catch语句的底部放置一个catch (...)。这是因为你正在包装的库可能文档不完善。没有C++结构可以用来强制执行仅可抛出一组异常。哪个是两害相权取其轻?catch (...)还是冒险运行时崩溃,当包装的代码试图向C调用者抛出异常时? - figurassa
1
捕获 (...) {std::terminate();} 是被接受的。捕获 (...) {} 可能会存在安全漏洞。 - Terry Mahaffey
显示剩余3条评论

12

将C++代码暴露给C语言并不困难,只需使用外观设计模式。

我假设你的C++代码已经构建成一个库,你需要做的就是在C++库中创建一个C模块,作为库的外观,并提供一个纯C头文件。该C模块将调用相关的C++函数。

完成上述步骤后,C应用程序和库将完全可以访问你所暴露的C API。

例如,这里是一个外观模块的示例:

#include <libInterface.h>
#include <objectedOrientedCppStuff.h>

int doObjectOrientedStuff(int *arg1, int arg2, char *arg3) {
      Object obj = ObjectFactory->makeCppObj(arg3); // doing object oriented stuff here
      obj->doStuff(arg2);
      return obj->doMoreStuff(arg1);
   }

你可以将这个C函数作为API公开,然后你可以自由地将其用作C库,而不必担心

// file name "libIntrface.h"
extern int doObjectOrientedStuff(int *, int, char*);

显然这只是一个人为的例子,但这是将C++库公开给C的最简单的方法。


嗨@hhafez,你有一个简单的hello world例子吗?带有字符串的那种? - Jason Foglia
1
对于一个不熟悉 C++ 的人来说,这真是太好了。 - Nicklas Avén

6

我认为您可能可以从方向上获得一些想法,或者可能直接使用SWIG。我认为,查看一些示例至少可以让您了解在将一个API封装到另一个API时需要考虑哪些内容。这个练习可能会有益处。

SWIG是一种软件开发工具,可将用C和C ++编写的程序与各种高级编程语言相连接。 SWIG与不同类型的语言一起使用,包括常见的脚本语言,如Perl、PHP、Python、Tcl和Ruby。 支持的语言列表还包括非脚本语言,例如C#,Common Lisp(CLISP,Allegro CL,CFFI,UFFI),Java,Lua,Modula-3,OCAML,Octave和R。 还支持几种解释和编译Scheme实现(Guile,MzScheme,Chicken)。 SWIG最常用于创建高级解释或编译的编程环境、用户界面,并作为测试和原型设计C / C ++软件的工具。 SWIG还可以将其解析树以XML和Lisp s表达式形式导出。 SWIG可以自由地用于商业和非商业用途。


3
如果他只是想让一个 C++ 库能够从 C 中使用,那么使用 SWIG 有些过头了。 - hhafez
2
这只是一个观点,没有真正有用的反馈。如果原始代码正在快速更改,没有C++资源来维护它,只有C资源可用,并且开发人员想要自动化C API生成,则SWIG会很有帮助。这些都是常见且肯定有效的使用SWIG的原因。 - user1363990

5

只需要用void *(在面向C的库中通常称为不透明类型)替换对象的概念,然后重复使用你从C++中学到的一切即可。


3
我认为使用SWIG是最好的答案...不仅避免了重复造轮子,而且可靠,并促进了开发的连续性,而不是一次性解决问题。高频问题需要通过长期解决方案来解决。

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