当前位置 博文首页 > 丁劲犇技术发布空间:大道至简-基于C的库封装发布技术

    丁劲犇技术发布空间:大道至简-基于C的库封装发布技术

    作者:[db:作者] 时间:2021-09-19 22:43

    每年实验课,总有同学问我,如何生成DLL、如何导出类,如何不花很多时间精力,就设计出一个给别人用的爽的功能库呢?结合这些年的实践,我们今天就来聊一聊动态链接库的封装发布。您也可以直接跳到文章最后,去github查看C++/C混合库的经典案例——Ettus uhd

    要让自己的库好用,又通用,该怎么办?重要的事情说前面:

    • 不要导出类、不要导出变量,仅使用C基础数据类型。
    • 面向对象实现功能真香,实现接口真要命。
    • 用最棒的语言实现功能,遵循C语言标准实现接口。
    • 非密集吞吐的接口,可以使用json整体交互。密集吞吐,用内存。

    做到了这几点,即使用户从VC2010换成了python,库都不用改。究其原因,C++的类在二进制结构上是缺乏定义的,一个返回值用了std::string,或者参数用了CTime的方法,从VC2010导出的DLL到了VC2017就不一定能用,更别提其他编译器和语言了。
    在这里插入图片描述

    相关范例代码见https://codechina.csdn.net/coloreaglestdio/qtcpp_demo/-/tree/master/findfoo

    1. C动态链接库是一种即成标准

    C语言是一门古老的语言。从六七十年代开始,在Unix/Linux操作系统上,C语言实现了大量的库,几乎涵盖了当代科学涉及的所有领域。从基础的XML操作,到复杂的数学算法,都能找到对应的C库。C语言的动态链接库承载了太多的智力遗产,以至于后来的大部分语言都自觉的加入了享用既有C语言动态链接库的能力。

    这种情况使得符合C语言习惯的动态链接库接口1成为了一种即成实事,不同的语言之间,使用C动态链接库的标准交互。尽管这种接口是面向过程的,而可用的参数类型少的可怜,但其简单、直接,又有大量的历史资源,使得后来的CORBA、COM也无法取代这种底层的接口方式2

    这里有几个概念需要明确:

    • C接口的动态链接库的通用性,一般只和操作系统、运行时(32位还是64位)有关,和具体的编译器、语言无关。
    • 很多现代编程语言能调用C接口的动态链接库。
    • 部分现代编程语言能生成C接口的动态链接库。

    无论你使用什么语言开发功能,只要提供了符合C语言动态链接库结构的接口,许多其他语言就可以使用你的功能。因此,完全可以用C++语言实现一个C接口的库,在里面尽情使用STL
    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();
    }
    //用关键词查找rawStr
    long 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<long long, XXX>来管理各个实例,也可以把实例全部放在一个struct中。如果用std::map,切记多线程下的mutex一致性保护,防止用户同时在多个线程init好几组功能实例,导致std::map崩溃。

    从性能角度,建议采用struct来承载所有运行时,而后返回指向该struct的指针。

    2.2 导出这些方法

    上述函数,因为是C++函数,编译器会对其进行改名,把参数也放进去,以便支持多态(同一个函数名,不同参数)。要导出为C的函数,就不允许编译器改名字。要用“extern ‘C‘”进行包装,以便导出这些方法时,函数名不变。

    同时,在Windows下,函数存在多个参数时,栈内的参数顺序也有从左开始还是从右压栈的区别。要做到最大的适应性,需要指定 stdcall开关。

    最后,我们不想为生成库的工程、用户的工程准备两套头文件,故而需要一些琐碎的宏定义,以区分当前编译的是DLL本身,还是使用DLL的用户工程。

    具体:

    1. 建立一个头文件,叫做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宏,这样,就开启了导出开关。

    1. 建立头文件findfoo.h
    #ifndef FINDFOO_H
    #define FINDFOO_H
    #include "findfoo_global.h"
    #ifdef __cplusplus
    extern "C"{
    #endif
    FINDFOO_EXPORT FFHANDLE		FOOCALL		ff_init_task	(const char * task);
    FINDFOO_EXPORT void			FOOCALL		ff_reset_task	(FFHANDLE h	, const char * task);
    FINDFOO_EXPORT const char * FOOCALL		ff_get_task		(FFHANDLE h	);
    FINDFOO_EXPORT long long	FOOCALL		ff_find			(FFHANDLE h	, const char * rawStr);
    FINDFOO_EXPORT void			FOOCALL		ff_fini_task	(FFHANDLE h	);
    #ifdef __cplusplus
    }
    #endif
    #endif // FINDFOO_H
    
    
    1. 实现导出方法 findfoo.cpp
    #include "findfoo.h"
    #include <assert.h>
    #include <string>
    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;
    }