基于C语言的库封装发布技术详解
目录
- 1. C动态链接库是一种即成标准
- 2. 用C++制作C的库
- 2.1 使用void * 作为句柄
- 2.2 导出这些方法
- 3. 使用库
- 4. 经典的范例:libuhd
- 总结
要让自己的库好用,又通用,该怎么办?重要的事情说前面:
- 不要导出类、不要导出变量,仅使用C基础数据类型。
- 面向对象实现功能真香,实现接口真要命。
- 用最棒的语言实现功能,遵循C语言标准实现接口。
- 非密集吞吐的接口,可以使用json整体交互。密集吞吐,用内存。
1. C动态链接库是一种即成标准 C语言是一门古老的语言。从六七十年代开始,在Unix/Linux操作系统上,C语言实现了大量的库,几乎涵盖了当代科学涉及的所有领域。从基础的XML操作,到复杂的数学算法,都能找到对应的C库。C语言的动态链接库承载了太多的智力遗产,以至于后来的大部分语言都自觉的加入了享用既有C语言动态链接库的能力。
这种情况使得符合C语言习惯的动态链接库接口1成为了一种即成实事,不同的语言之间,使用C动态链接库的标准交互。尽管这种接口是面向过程的,而可用的参数类型少的可怜,但其简单、直接,又有大量的历史资源,使得后来的CORBA、COM也无法取代这种底层的接口方式2。
这里有几个概念需要明确:
- C接口的动态链接库的通用性,一般只和操作系统、运行时(32位还是64位)有关,和具体的编译器、语言无关。
- 很多现代编程语言能调用C接口的动态链接库。
- 部分现代编程语言能生成C接口的动态链接库。
文章图片
2. 用C++制作C的库 用C++做C的库,关键是用好句柄。
什么是“句柄(Handle)”?这是个翻译问题。你可以理解为“把手”或者“提手”更合适。句柄很多时候是一个整数,用于标记一堆运行时资源,实现操作动态库功能的目的。
对一个复杂的功能来说,需要很多运行时的参数来支撑。比如FFT,就需要有一个内存区域记录蝶形运算的单元,以及指向各层单元的索引。对通信中的纠错译码,需要一些内存区域记忆寄存器,以及当前的状态。所有上述这些状态,都可以用一个struct 包裹起来,形成一个“箱子”。这个箱子对用户是透明的,只需要把箱子的把手(Handle)交给用户手上,用户在需要的时候,交回箱子并执行任务。
文章图片
不难想像,可以同时申请多个箱子,交给不同的线程去执行。库的设计者要确保Handle标记的参数包之间是独立的、线程安全的。
同时,句柄本身可以复刻面向对象的部分功能。如果把Handle作为this指针看待,则C++类可以直接导出为C的函数。只是首个参数要传入Handle即可。
2.1 使用void * 作为句柄
举个例子,假设手头有一个实现字符串查找的类,需要向外发布功能。但这个类是C++的,类似:
//关键词查找器类class Findfoo{public: Findfoo(const std::string & task = "foo"); ~Findfoo(); public: void setTask(const std::string & task); const std::string &task() const; //在rawStr里查找关键词 long long Find(const std::string & rawStr); private: //用于匹配的关键词 std::string m_task = "foo"; }; Findfoo::Findfoo(const std::string & task) :m_task(task){}Findfoo::~Findfoo(){}void Findfoo::setTask(const std::string & task){ m_task = task; }const std::string &Findfoo::task() const{ return m_task; }long long Findfoo::Find(const std::string & rawStr){ return rawStr.find(m_task); }
此时,可以设置以下接口,把C++的类变成C的方法。一旦变为C的方法,外部就无需知道该类的存在。
//创建一个查找器,返回句柄。提供的是关键词。void * ff_init_task(const char * task){ Findfoo * f = new Findfoo(task); return (void *) f; }//重设关键词void ff_reset_task(void * h, const char * task){ Findfoo * f = (Findfoo *)(h); assert(f); f->setTask(task); }//获取当前关键词const char * ff_get_task(void * h){ Findfoo * f = (Findfoo *)(h); assert(f); return f->task().c_str(); }//用关键词查找rawStrlong long ff_find(void * h, const char * rawStr){ Findfoo * f = (Findfoo *)(h); assert(f); return f->Find(rawStr); }//删除当前查找器void ff_fini_task(void * h){ Findfoo * f = (Findfoo *)(h); if (f)delete f; }
如此操作,用户可以完全不知道存在Findfoo类,只用一个void *指针作为操作类的指示。
上面的例子仅有1个类作为演示。实际开发中,一个工作可能由好几个类的实例共同协作完成。可以用一个std::map
从性能角度,建议采用struct来承载所有运行时,而后返回指向该struct的指针。
2.2 导出这些方法
上述函数,因为是C++函数,编译器会对其进行改名,把参数也放进去,以便支持多态(同一个函数名,不同参数)。要导出为C的函数,就不允许编译器改名字。要用“extern ‘C‘”进行包装,以便导出这些方法时,函数名不变。
同时,在Windows下,函数存在多个参数时,栈内的参数顺序也有从左开始还是从右压栈的区别。要做到最大的适应性,需要指定 stdcall开关。
最后,我们不想为生成库的工程、用户的工程准备两套头文件,故而需要一些琐碎的宏定义,以区分当前编译的是DLL本身,还是使用DLL的用户工程。
具体:
建立一个头文件,叫做findfoo_global.h。这个头文件对Linux和windows平台定义一些宏,用于声明函数时,指定导出(构造DLL本身)和导入(使用DLL)
#ifndef FINDFOO_GLOBAL_H#define FINDFOO_GLOBAL_H#if defined(_MSC_VER) || defined(WIN64) || defined(_WIN64) || defined(__WIN64__) || defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)#define Q_DECL_EXPORT __declspec(dllexport)#define Q_DECL_IMPORT __declspec(dllimport)#define FOOCALL __stdcall#else#define Q_DECL_EXPORT__attribute__((visibility("default")))#define Q_DECL_IMPORT__attribute__((visibility("default")))#define FOOCALL#endif#if defined(FINDFOO_LIBRARY)#define FINDFOO_EXPORT Q_DECL_EXPORT#else#define FINDFOO_EXPORT Q_DECL_IMPORT#endif//句柄就是void *#define FFHANDLE void *#endif // FINDFOO_GLOBAL_H
在DLL的工程中,要定义FINDFOO_LIBRARY宏,这样,就开启了导出开关。
建立头文件findfoo.h
#ifndef FINDFOO_H#define FINDFOO_H#include "findfoo_global.h"#ifdef __cplusplusextern "C"{#endifFINDFOO_EXPORT FFHANDLEFOOCALLff_init_task (const char * task); FINDFOO_EXPORT voidFOOCALLff_reset_task (FFHANDLE h , const char * task); FINDFOO_EXPORT const char * FOOCALLff_get_task(FFHANDLE h ); FINDFOO_EXPORT long long FOOCALLff_find(FFHANDLE h , const char * rawStr); FINDFOO_EXPORT voidFOOCALLff_fini_task (FFHANDLE h ); #ifdef __cplusplus}#endif#endif // FINDFOO_H
实现导出方法 findfoo.cpp
#include "findfoo.h"#include #include class Findfoo{public: Findfoo(const std::string & task = "foo"); ~Findfoo(); public: void setTask(const std::string & task); const std::string &task() const; long long Find(const std::string & rawStr); private: std::string m_task = "foo"; }; Findfoo::Findfoo(const std::string & task) :m_task(task){}Findfoo::~Findfoo(){}void Findfoo::setTask(const std::string & task){ m_task = task; }const std::string &Findfoo::task() const{ return m_task; }long long Findfoo::Find(const std::string & rawStr){ return rawStr.find(m_task); }//-----------FINDFOO_EXPORT FFHANDLE FOOCALL ff_init_task(const char * task){ Findfoo * f = new Findfoo(task); return (FFHANDLE) f; }FINDFOO_EXPORT void FOOCALL ff_reset_task(FFHANDLE h, const char * task){ Findfoo * f = reinterpret_cast(h); assert(f); f->setTask(task); }FINDFOO_EXPORT const char * FOOCALL ff_get_task(FFHANDLE h){ Findfoo * f = reinterpret_cast (h); assert(f); return f->task().c_str(); }FINDFOO_EXPORT long long FOOCALL ff_find(FFHANDLE h, const char * rawStr){ Findfoo * f = reinterpret_cast (h); assert(f); return f->Find(rawStr); }FINDFOO_EXPORT void FOOCALL ff_fini_task(FFHANDLE h){ Findfoo * f = reinterpret_cast (h); if (f)delete f; }
3. 使用库 一旦导出了上述方法,即可使用库。
#include#include #include "findfoo.h"using namespace std; int main(){ FFHANDLE h = ff_init_task("foobar"); assert(h); cout << "Task string:" << ff_get_task(h) << endl; cout << "Input String:"; std::string strRaw; cin >> strRaw; cout << ff_find(h,strRaw.c_str()); //Delete ff_fini_task(h); h = nullptr; return 0; }
上述是最简单的例子。当需要处理大量动态内存时,需要注意:内存谁申请,谁释放。这一点特别容易引起错误。
4. 经典的范例:libuhd USRP软件无线电平台对应的开源库libuhd是用C++ boost开发的。但是,为了兼容更多的语言,其进行了封装,把各个类都用句柄抽象出来了,且是标准C的接口。
可以去Github的工程页签出项目查看,也可以跟踪代码查看其原理。
这个项目是把C、C++的联合运用发挥的非常棒的例子。
1包括函数入口点的定位方式、函数命名方式、参数传递规则、参数类型。
2.COM和C接口DLL其实不是一个范畴的东西,这里放在一起,有点粗暴。
总结 本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注脚本之家的更多内容!
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量