基于C语言的库封装发布技术详解

目录
  • 1. C动态链接库是一种即成标准
  • 2. 用C++制作C的库
    • 2.1 使用void * 作为句柄
    • 2.2 导出这些方法
  • 3. 使用库
  • 4. 经典的范例:libuhd
  • 总结

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

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

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

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

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。

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的用户工程。

具体:

建立一个头文件,叫做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 __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

实现导出方法 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;
}
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<Findfoo *>(h);
	assert(f);
	f->setTask(task);
}
FINDFOO_EXPORT const char * FOOCALL ff_get_task(FFHANDLE h)
{
	Findfoo * f = reinterpret_cast<Findfoo *>(h);
	assert(f);
	return f->task().c_str();
}
FINDFOO_EXPORT long long FOOCALL ff_find(FFHANDLE h, const char * rawStr)
{
	Findfoo * f = reinterpret_cast<Findfoo *>(h);
	assert(f);
	return f->Find(rawStr);
}
FINDFOO_EXPORT void FOOCALL ff_fini_task(FFHANDLE h)
{
	Findfoo * f = reinterpret_cast<Findfoo *>(h);
	if (f)
		delete f;
}

3. 使用库

一旦导出了上述方法,即可使用库。

#include <iostream>
#include <cassert>
#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其实不是一个范畴的东西,这里放在一起,有点粗暴。

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 纯c语言优雅地实现矩阵运算库的方法

    目录 1.一个优雅好用的c语言库必须满足哪些条件 2.实现一个矩阵运算库的几点思考 (1)采用预定义的数据类型,避免直接使用编译器定义的数据类型 (2)基于对象编程,定义矩阵对象 (3)除了特别编写的内存处理函数(使用栈链表保存.释放动态分配的内存地址),不允许任何函数直接分配和释放内存 (4)防御性编程,对输入参数做有效性检查,并返回错误号 (5)注意编程细节的打磨 3.完整c程序 参考资料 编程既是技术输出也是艺术创作.鉴赏高手写的程序,往往让人眼前一亮,他们思路.逻辑清晰,所呈现的代码简洁

  • C语言使用Bresenham算法生成直线(easyx图形库)

    Bresenham算法是计算机图形学领域使用最广泛的直线扫描转换方法. 其原理是:过各行.各列像素中心构造一组虚拟网格线,按直线从起点到终点的顺序计算直线各垂直网格线的交点,然后确定该列像素中与此交点最近的像素. Bresenham算法也是一种计算机图形学中常见的绘制直线的算法,其本质思想也是步进的思想,但由于避免了浮点运算,相当于DDA算法的一种改进算法. 源代码展示: #include<stdio.h> #include<graphics.h> #include<math

  • C语言中操作sqlserver数据库案例教程

    本文使用c语言来对sql server数据库进行操作,实现通过程序来对数据库进行增删改查操作. 操作系统:windows 10         实验平台:vs2012  +  sql server 2008 ODBC简介:开放数据库连接(Open Database Connectivity,ODBC),主要的功能是提供了一组用于数据库访问的编程接口,其主要的特点是,如果应用程序使用ODBC做数据源,那么这个应用程序与所使用的数据库或数据库引擎是无关的,为应用程序的跨平台和可移植奠定了基础. 创建

  • C语言库的封装和使用方法总结

    目录 前言 windows下静态库创建和使用 静态库的创建 静态库的使用 方法一:添加工程中 方法二:配置项目属性 方法三:使用编译语句 静态库优缺点 缺点 windows下动态库创建和使用 静态库中生成的.lib和动态库生成的.lib是不同的 __declspec(dllexport)是什么意思? 动态库的lib文件和静态库的lib文件的区别? 动态库的使用 方法一:隐式调用 方法二:添加工程中(如静态库的使用中方法一) 方法三:显式调用 总结 前言 库是已经写好的.成熟的.可复用的代码.在我

  • 基于C语言的库封装发布技术详解

    目录 1. C动态链接库是一种即成标准 2. 用C++制作C的库 2.1 使用void * 作为句柄 2.2 导出这些方法 3. 使用库 4. 经典的范例:libuhd 总结 每年实验课,总有同学问我,如何生成DLL.如何导出类,如何不花很多时间精力,就设计出一个给别人用的爽的功能库呢?结合这些年的实践,我们今天就来聊一聊动态链接库的封装发布.您也可以直接跳到文章最后,去github查看C++/C混合库的经典案例--Ettus uhd 要让自己的库好用,又通用,该怎么办?重要的事情说前面: 不要

  • 基于HTTP协议的一些实时数据获取技术详解

    HTTP协议 HTTP协议大家都很熟悉了,开始本文之前,首先简单回顾一下HTTP协议. HTTP协议是建立在TCP协议上的应用层协议,协议的本质是请求----应答: 即对于HTTP协议来说,服务端给一次响应后整个请求就结束了,这是HTTP请求最大的特点,也是由于这个特点,HTTP请求无法做到的是服务端向客户端主动推送数据. 但由于HTTP协议的广泛应用,很多时候确实又想使用HTTP协议去实现实时的数据获取,这种时候应当怎么办呢?下面首先介绍几种基于HTTP协议的实时数据获取方法. 短轮询 轮询是

  • 基于C语言EOF与getchar()的使用详解

    大师级经典的著作,要字斟句酌的去读,去理解.以前在看K&R的The C Programming Language(SecondEdition)第1.5节的字符输入/输出,被getchar()和EOF所迷惑了.可能主要还是由于没有搞清楚getchar()的工作原理和EOF的用法.因此,感觉很有必要总结一下,不然,很多琐碎的知识点长时间过后就会淡忘的,只有写下来才是最好的方法. 其实,getchar()最典型的程序也就几行代码而已.本人所用的环境是DebianGNU/Linux,在其他系统下也一样.

  • Django框架之DRF 基于mixins来封装的视图详解

    基础视图 示例环境搭建:新建一个Django项目,连接Mysql数据库,配置路由.视图函数.序列化单独创建py文件 # 配置路由 from django.conf.urls import url from django.contrib import admin from app01 import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^PublishView/', views.PublishView.as_vi

  • Java WebService技术详解

    目录 WebService WebService简介 WebService原理 JAVA WebService规范 (1)JAX-WS: (2)JAXM&SAAJ: (3)JAX-RS: WebService入门案例 服务端的实现 客户端的实现 WSDL 文档结构 阅读方式 SOAP SOAP结构 UDDI Webservice的客户端调用方式 一:生成客户端调用方式 二:service编程调用方式 三:HttpURLConnection调用方式 使用注解修改WSDL内容 WebService

  • Go语言中的数据竞争模式详解

    目录 前言 Go在goroutine中通过引用来透明地捕获自由变量 切片会产生难以诊断的数据竞争 并发访问Go内置的.不安全的线程映射会导致频繁的数据竞争 Go开发人员常在pass-by-value时犯错并导致non-trivial的数据竞争 消息传递(通道)和共享内存的混合使用使代码变得复杂且易受数据竞争的影响 Add和Done方法的错误放置会导致数据竞争 并发运行测试会导致产品或测试代码中的数据竞争 小结 前言 本文主要基于在Uber的Go monorepo中发现的各种数据竞争模式,分析了其

  • 基于MyBatis的数据持久化框架的使用详解

    目录 一.MyBatis是什么 1.1.概述 1.2.什么是持久化 1.3.什么是ORM 1.4.MyBatis主要内容 1.5.优点 1.6.缺点 二.MyBatis架构 2.1.mybatis所依赖的jar包 2.2.MyBatis准备工作 三.MyBatis 核心对象 一.MyBatis是什么 1.1.概述 Mybatis是一个优秀的开源.轻量级持久层框架,它对JDBC操作数据库的过程进行封装,简化了加载驱动.创建连接.创建 statement 等繁杂的过程,使开发者只需要关注sql本身.

  • 基于使用paramiko执行远程linux主机命令(详解)

    paramiko是python的SSH库,可用来连接远程linux主机,然后执行linux命令或者通过SFTP传输文件. 关于使用paramiko执行远程主机命令可以找到很多参考资料了,本文在此基础上做一些封装,便于扩展与编写脚本. 下面直接给出代码: # coding: utf-8 import paramiko import re from time import sleep # 定义一个类,表示一台远端linux主机 class Linux(object): # 通过IP, 用户名,密码,

  • Java语言中的内存泄露代码详解

    Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同. JAVA中的内存管理 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的. 在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上. 下面看一个示例: public class Simple { public static vo

  • python PaddleOCR库用法及知识点详解

    说明 1.PaddleOCR是基于深度学习的ocr识别库,中文识别精度相当还不错,能够应对大多数文字提取需求. 2.需要依次安装三个依赖库,shapely库可能会受到系统的影响,出现安装错误. 安装命令 pip install paddlepaddle pip install shapely pip install paddleocr 代码实现 ocr = PaddleOCR(use_angle_cls=True,) # 输入待识别图片路径 img_path = r"d:\Desktop\4A3

随机推荐