为我的 C++ 应用程序提供 SDK

2022-01-08 00:00:00 sdk c++

假设我正在用 C++ 创建一个游戏引擎,我只想提供一些头文件而不是提供整个源代码,而这些头文件将需要创建新的游戏实例、提供脚本类、提供游戏对象类以及组件、数学等.

Let's say that I'm creating a game engine in C++ and I want to provide only some headers instead of providing whole source code, and those headers will be needed to create new game instance, provide Script class, provide game object class and components, math, etc..

是的,显然我想为我的游戏引擎提供 SDK,但是如何做到这一点,如何只提供一些公共标头并隐藏源文件和仅引擎标头?如何将这些标题链接到源的其余部分?

Yeah obviously I want to provide SDK for my game engine but how to do it, how to provide only some public headers and hide source files and engine only headers? How to link those headers to the rest of the source?

我在 Linux 平台上使用 Eclipse CDT.

I'm using Eclipse CDT on Linux platform.

推荐答案

通常,通过提供共享(动态)库并在标头中提供纯虚拟接口,您将获得最佳(轻松)可实现的二进制兼容性,一些外部 C 入口点(为了交叉编译器兼容性,因为每个编译器对 C++ 名称的修改方式不同).

In general, youd'd get the best (easily) achievable binary compatibility by providing a shared (dynamic) library and provide pure virtual interface in the headers, with some extern C entry points (for cross-compiler compatibility, as C++ names are mangled by each compiler differently).

这篇文章可能是一个很好的起点:http://chadaustin.me/cppinterface.html- 它主要针对Windows,但也可以应用于Linux.

A good starting point could be this article: http://chadaustin.me/cppinterface.html - it is mainly targeted to Windows, however it can be applied to Linux as well.

在设计共享库(在 Windows 和 Linux 中工作)时,我实际上已将其用作起点,但我放弃了自定义运算符 delete 以支持直接调用 destroy 方法(实际上是通过自定义智能指针).

I've actually used that as a starting point when designing a shared library (working in both Windows and Linux), but I dropped the custom operator delete in favor of calling the destroy method directly (actually by a customized smart pointer).

在Linux下,也建议使用编译器的visibility"标志,默认隐藏所有内容(-fvisibility=hidden")并且只标志为__attribute__((visibility(default")))需要导出的函数(注意只需要导出extern "C"入口点,纯虚接口不需要导出).

Under Linux, it is also advisable to use the "visibility" flag of the compiler, make everything hidden by default ("-fvisibility=hidden") and only flag as __attribute__ ((visibility ("default"))) the functions which need to be exported (note that you only need to export the extern "C" entry point and the pure virtual interfaces do not need to be exported).

为了更好的二进制兼容性,您甚至需要避免使用虚拟方法并实现自己的虚拟表(与用户可能使用的每个编译器兼容),但纯虚拟接口实际上足够兼容.

For even better binary compatibility you would need to avoid even virtual methods and implement your own virtual tables (compatible with every compiler the user might use), but pure virtual interfaces are actually compatible reasonably enough.

使用静态库您可能会遇到问题,因为您可能需要为用户可能使用的每个编译器(有时甚至是同一编译器的不同版本)提供一个静态库.

With static libraries you might have issues, because you might then need to provide a static library for every compiler (and sometimes even different versions of the same compiler) the user might use.

例如,界面可能如下所示:

As an example, the interface might look like:

class Interface {
public:
   virtual void destroy() = 0;
protected:
   // prevent to call delete directly
   // - needs to be done in every public interface class
   ~Interface() {}
};

class IGameComponent: public Interface {
public:
    virtual int32_t someMethod() const = 0;
protected:
   ~IGameComponent() {}
};

class IGameEngine: public Interface {
public:
    // call component->destroy() when done with the component
    virtual IGameComponent * createComponent() const = 0;
protected:
   ~IGameComponent() {}
};

extern "C"
__attribute__ ((visibility ("default")))
IGameEngine * createEngine();

实现如下所示:

// CRTP to avoid having to implement destroy() in every implementation
template< class INTERFACE_T >
class InterfaceImpl: public INTERFACE_T {
public:
   virtual void destroy() { delete this; }
   virtual ~InterfaceImpl() {}
};

class GameComponentImpl: public InterfaceImpl<IGameComponent> {
public:
    virtual int32_t someMethod() const
    { return 5; }
};

class GameEngineImpl: public InterfaceImpl<IGameEngine> {
public:
    virtual IGameComponent * createComponent() const
    {
        try {
            return new GameComponentImpl;
        } catch (...) {
            // log error
            return NULL;
        }
    }
};

extern "C"
IGameEngine * createEngine()
{
    try {
        return new GameEngineImpl;
    } catch (...) {
        // log error
        return NULL;
    }
}

这是我实现库接口的原理.建议将分配的对象包装在一个智能 ptr 中,但要对其进行定制,以便它调用 Interface::destroy() 而不是 delete.

This is in the principle how I implemented the interface of the library. It is advisable to wrap the allocated objects inside a smart ptr, but customized so that it calls Interface::destroy() instead of delete.

还要注意 int32_t 的使用――一般来说,如果你希望接口尽可能地与交叉编译器兼容,你应该使用固定大小的类型(即不是例如 size_t,这也适用于 bool 和 enums,它们都高度依赖于编译器,但对于 int、short、long 等也是如此).

Also note the use of int32_t - in general, if you want the interface to be as cross-compiler compatible as possible, you should use fixed size types (i.e. not for example size_t, and that also applies to bool and enums, which all are highly compiler dependent, but even so for int, short, long etc.).

进一步注意 try/catch 保护的使用,一般来说,如果您希望 API 可能在不同的编译器中使用(或者有时甚至在调试/非调试版本的相同的编译器,但更适用于 Windows;但是当库与太多不同的版本(例如 GCC 编译器)一起使用时仍然可能会出现问题.

Further note the use of try/catch guards, in general you should not allow exceptions to pass the API boundary if you expect the API might be used from a different compiler (or sometimes even between debug/non-debug versions of the same compiler, but that applies more to Windows; however there still could be issues when the library will be used with too different version of e.g. the GCC compiler).

相关文章