在dll中使用第三方库

摘要

上一篇在VS中创建了一个dll项目和一个C++客户端项目用来测试dll的功能。在本篇中会在dll中调用第三方库。其实在C++的控制台项目中和在dll项目中调用并没有太大的区别,都可以参考本文。

C++三方库介绍

俗话说的好,“不重复造轮子”,C++的生态虽然没有python方便,但还是有不少库能够提高开发效率的。除了可以直接调用的C++标准库外,还有比较出名的比如Boost、OpenCV等比较常用的库。

一般来说三方库可以分为两种,一种是提供了源码/编译好的源码,另一种是head-only的库。C++程序员会比较喜欢headonly的,因为可以直接#include,非常方便。这里我们展示两个库的使用,一个是spdlog,一个是FFTW,其他库的原理都是差不多的,学会之后就可以自行处理。

spdlog

spdlog是一个快速、异步的日志库,仓库地址 https://github.com/gabime/spdlog

下载好源代码,然后将include目录下的spdlog文件夹拷贝到我们解决方案目录下,新建一个目录取名为thridarty,专门用来放第三方库的代码。右击dll项目选择“配置属性”>“C/C++”>“常规”将thridarty添加到附加包含目录中 直接引用会报错,static_assert failed: 'Unicode support requires compiling with /utf-8',需要在属性页的“C/C++”>“命令行”中填写“/utf-8”可以实现配置,参考文档

可以编写下面的直接开始使用spdlog(默认输出在控制台)

1
2
3
4
5
6
7
8
9
#include "spdlog/spdlog.h"

spdlog::trace("这是一条 trace 级别的日志(最详细)");
spdlog::debug("这是一条 debug 级别的日志");
spdlog::info("这是一条 info 级别的日志");
spdlog::warn("这是一条 warn 级别的日志");
spdlog::error("这是一条 error 级别的日志");
spdlog::critical("这是一条 critical 级别的日志(最严重)");

spdlog使用方法

这里顺带介绍一下spdlog库的使用参考本文。如果不需要可以跳过这部分,如果想直接嵌入工程,可以拷贝下一节的代码。

spdlog 的架构设计清晰,主要包含三个核心组件。

  1. Formatter(格式化器)

Formatter 负责控制日志的输出格式,决定了每条日志包含哪些信息以及如何展示。spdlog 提供了合理的默认格式,包含日期时间、日志器名称、日志级别和日志内容。通过 set_pattern 方法可以自定义格式。

1
2
3
4
// 全局设置格式
spdlog::set_pattern("[%H:%M:%S %z] [thread %t] %v");
// 单个日志器设置格式
some_logger->set_pattern(">>> %H:%M:%S %v <<<");
  1. Sink(通道)

Sink 是日志的输出目的地,每个 sink 都关联着一个 formatter,负责将格式化后的日志输出到指定位置。指定位置可以是控制台,也可以是文件

控制台:输出到命令行终端,支持彩色显示,_mt 后缀表示多线程安全版本:

1
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

基础文件

1
auto basic_file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log.txt");

每日轮转的Sink

1
2
// 每天 14:22 生成新文件
auto daily_sink = std::make_shared<spdlog::sinks::daily_file_sink_mt>("log", 14, 22);

大小轮转的Sink

1
2
// 单个文件最大 10MB,最多保留 100 个文件
auto rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("log", 10*1024*1024, 100);

在写入文件的时候为避免日志缓存导致输出延迟,建议设置刷新策略

1
2
3
4
// 每 1 秒自动刷新
spdlog::flush_every(std::chrono::seconds(1));
// 当日志级别为 debug 及以上时立即刷新
spdlog::flush_on(spdlog::level::debug);
  1. Logger(日志器)

Logger 是日志操作的入口,每个日志器包含一个或多个 sink,日志信息会通过所有关联的 sink 输出。

默认日志器:spdlog 提供全局默认日志器,输出到控制台,多线程安全且支持彩色显示。 直接使用

1
spdlog::info("这是默认日志器输出的信息");

自定义日志器:可根据需求创建特定功能的日志器,有两种创建方式:

直接创建:适用于单个 sink 的场景

1
2
3
4
// 创建控制台日志器
auto console_logger = spdlog::stdout_color_mt("console_logger");
// 创建轮转文件日志器
auto file_logger = spdlog::rotating_logger_mt("file_logger", "log.txt", 10*1024*1024, 100);

组合 sink 创建:适用于多个 sink 的场景

1
2
3
4
5
6
std::vector<spdlog::sink_ptr> sinks;
sinks.push_back(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
sinks.push_back(std::make_shared<spdlog::sinks::rotating_file_sink_mt>("log.txt", 10*1024*1024, 100));
auto combined_logger = std::make_shared<spdlog::logger>("combined_logger", sinks.begin(), sinks.end());
// 注册日志器使其全局可用
spdlog::register_logger(combined_logger);

日志和sink都可以设置日志级别,只有达到指定级别的日志才会被输出,级别从低到高排列为 trace -> debug -> info -> warn -> error -> critical。

1
2
3
4
// 设置日志器级别为 debug
combined_logger->set_level(spdlog::level::debug);
// 设置 sink 级别为 info(仅输出 info 及以上级别)
sinks[0]->set_level(spdlog::level::info);

将spdlog嵌入dll项目

这里新建一个logger.h和logger.cpp文件将spdlog封装起来,在dll项目里新建两个文件,将下面的代码进行复制

logger.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#pragma once

#include <string>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/daily_file_sink.h"

// 日志工具类,单例模式
class Logger
{
public:
// 初始化日志系统
static bool Init(const std::string& log_dir, const std::string& log_name,
int hour = 0, int minute = 0);

// 获取日志器实例
static std::shared_ptr<spdlog::logger> GetLogger();

// 禁用拷贝构造和赋值操作
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;

private:
Logger() = default;
~Logger() = default;

static std::shared_ptr<spdlog::logger> s_logger;
};

// 日志宏定义,方便使用
#define LOG_TRACE(...) ::Logger::GetLogger()->trace(__VA_ARGS__)
#define LOG_DEBUG(...) ::Logger::GetLogger()->debug(__VA_ARGS__)
#define LOG_INFO(...) ::Logger::GetLogger()->info(__VA_ARGS__)
#define LOG_WARN(...) ::Logger::GetLogger()->warn(__VA_ARGS__)
#define LOG_ERROR(...) ::Logger::GetLogger()->error(__VA_ARGS__)
#define LOG_CRITICAL(...) ::Logger::GetLogger()->critical(__VA_ARGS__)

logger.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include "logger.h"
#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

// 静态成员初始化
std::shared_ptr<spdlog::logger> Logger::s_logger = nullptr;

bool Logger::Init(const std::string& log_dir, const std::string& log_name, int hour, int minute)
{
try
{
// 检查并创建日志目录
if (!fs::exists(log_dir))
{
fs::create_directories(fs::path(log_dir));
}

// 日志文件路径
std::string log_file = log_dir + "/" + log_name;

// 设置日志格式
//spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");

// 创建日志器
s_logger = spdlog::daily_logger_mt("logger", log_file, hour, minute);

// 设置日志级别
s_logger->set_level(spdlog::level::trace);

// 设置刷新策略:ERROR 及以上立即刷新 + 每3秒定时刷新
s_logger->flush_on(spdlog::level::err);
spdlog::flush_every(std::chrono::seconds(3));

// 注册为默认日志器
spdlog::set_default_logger(s_logger);

return true;
}
catch (const std::exception& e)
{
// 初始化失败时输出错误信息
std::cerr << "Logger initialization failed: " << e.what() << std::endl;
return false;
}
}

std::shared_ptr<spdlog::logger> Logger::GetLogger()
{
if (!s_logger)
{
throw std::runtime_error("Logger not initialized. Call Init() first.");
}
return s_logger;
}

修改dllmain.cpp的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include<windows.h>
#include"include/logger.h"

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// 初始化日志系统,每天凌晨2点轮转
Logger::Init("./logs","dll_log", 2, 0);
LOG_INFO("DLL loaded successfully");
break;
case DLL_THREAD_ATTACH:
LOG_DEBUG("New thread attached");
break;
case DLL_THREAD_DETACH:
LOG_DEBUG("Thread detached");
break;
case DLL_PROCESS_DETACH:
LOG_INFO("DLL unloaded");
spdlog::shutdown(); // 关闭日志系统
break;
}
return TRUE;
}

在MathLibaray.cpp的init这里添加测试函数(需要引用#include "logger.h")

1
2
3
4
5
6
7
8
9
10
11
12
void fibonacci_init(
const unsigned long long a,
const unsigned long long b)
{
index_ = 0;
current_ = a;
previous_ = b; // see special case when initialize
// 输出不同级别的日志(从低到高:trace, debug, info, warn, error, critical)
LOG_TRACE("Entering DoSomething()");
LOG_DEBUG("Performing some operation");
LOG_INFO("DoSomething() executed successfully");
}

在测试台端,可以注释掉一半的代码,重新生成dll,并运行测试代码,会在测试客户端工程下面生成一个logs文件,里面会有一个写有当前日期的文件

logs文件

我这里运行了两次,所以有两块文本。

FFTW

FFTW(Fastest Fourier Transform in the West)是一个高效的开源计算库,专门用于快速傅里叶变换(FFT)及其相关操作。FFTW不是一个headonly的库,它需要用户使用dll或者自己集成,不过FFTW提供了编译好的版本可以直接下载http://www.fftw.org/download.html 这里我们使用Windows64位版本的。

编译好的版本有dll和h文件,我们需要再编译出一个lib文件用于在Visual Studio中静态链接,需要用vs提供的lib.exe工具。

打开VS的"x64 Native Tools Command Prompt for VS 2022"工具,用该工具进入到解压的文件夹下,运行指令

1
lib /machine:x64 /def:libfftw3-3.def

取出这三个文件- "fftw3.h" "libfftw3-3.dll" "libfftw3-3.lib"

fftw文件

在thridarty文件夹下新建一个fftw的文件夹,将.h和.lib文件放进去,(可以把dll文件也放进去然后在运行后让vs帮忙复制),dll文件放到解决方案路径下的平台debug或release文件夹内。(这里因为不会涉及变动,直接放在编译文件夹下就可以了)

在“配置属性”>“链接器”>“常规” >“附加库目录”中输入../thridarty/fftw

在“输入”>“附加依赖项”中添加libfftw3-3.lib

在头文件和源文件下添加 fft_utils.hfft_utils.cpp ,在fft_utils.h里包含头文件fftw3.h将fft做一个封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

#ifndef FFT_UTILS_H
#define FFT_UTILS_H

#include <vector>
#include <complex>
#include <mutex>
#include <unordered_map>
#include <fftw/fftw3.h>

/**
* @brief FFT工具类,提供静态方法实现FFT和IFFT功能
* 模仿Python的numpy.fft接口风格,同时利用FFTW库实现高效计算
*/
class FFTUtils {
public:
// 复数类型定义,与std::complex<double>兼容
using Complex = std::complex<double>;

/**
* @brief 对实数输入执行FFT
* @param input 实数输入向量
* @return 复数输出向量,大小与输入相同
*/
static std::vector<Complex> fft(const std::vector<double>& input);

/**
* @brief 对复数输入执行FFT
* @param input 复数输入向量
* @return 复数输出向量,大小与输入相同
*/
static std::vector<Complex> fft(const std::vector<Complex>& input);

/**
* @brief 对复数输入执行IFFT
* @param input 复数输入向量
* @param normalize 是否归一化(默认true,与numpy一致)
* @return 复数输出向量,大小与输入相同
*/
static std::vector<Complex> ifft(const std::vector<Complex>& input, bool normalize = true);

/**
* @brief 计算复数向量的幅度谱
* @param input 复数输入向量
* @return 幅度谱(实数向量)
*/
static std::vector<double> magnitude(const std::vector<Complex>& input);

/**
* @brief 计算复数向量的相位谱
* @param input 复数输入向量
* @param epsilon 避免除零的小值
* @return 相位谱(实数向量,单位:弧度)
*/
static std::vector<double> phase(const std::vector<Complex>& input, double epsilon = 1e-10);

/**
* @brief 清除内部缓存的FFT计划,释放内存
*/
static void clearCache();

private:
// FFT计划的缓存结构
struct PlanCache {
fftw_plan forward_plan = nullptr; // 正向FFT计划
fftw_plan inverse_plan = nullptr; // 逆向FFT计划
fftw_complex* in = nullptr; // 输入缓冲区
fftw_complex* out = nullptr; // 输出缓冲区
};

// 缓存映射:大小 -> 计划缓存
static std::unordered_map<size_t, PlanCache> plan_cache_;
static std::mutex mutex_; // 线程安全的互斥锁

/**
* @brief 获取指定大小的FFT计划缓存
* @param size FFT大小
* @return 计划缓存的引用
*/
static PlanCache& getPlanCache(size_t size);

/**
* @brief 确保缓存中存在指定大小的计划
* @param size FFT大小
*/
static void ensurePlanExists(size_t size);

// 禁止实例化
FFTUtils() = delete;
~FFTUtils() = delete;
FFTUtils(const FFTUtils&) = delete;
FFTUtils& operator=(const FFTUtils&) = delete;
};

#endif // FFT_UTILS_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#include "fft_utils.h"
#include <stdexcept>
#include <cmath>
#include <algorithm>

// 初始化静态成员
std::unordered_map<size_t, FFTUtils::PlanCache> FFTUtils::plan_cache_;
std::mutex FFTUtils::mutex_;

FFTUtils::PlanCache& FFTUtils::getPlanCache(size_t size) {
std::lock_guard<std::mutex> lock(mutex_);
ensurePlanExists(size);
return plan_cache_[size];
}

void FFTUtils::ensurePlanExists(size_t size) {
if (plan_cache_.find(size) != plan_cache_.end()) {
return; // 计划已存在
}

// 创建新的缓存项
PlanCache cache;

// 分配对齐内存
cache.in = fftw_alloc_complex(size);
cache.out = fftw_alloc_complex(size);

if (!cache.in || !cache.out) {
throw std::bad_alloc();
}

// 创建正向和逆向FFT计划
cache.forward_plan = fftw_plan_dft_1d(
static_cast<int>(size),
cache.in,
cache.out,
FFTW_FORWARD,
FFTW_ESTIMATE // 快速创建计划,不进行耗时的优化
);

cache.inverse_plan = fftw_plan_dft_1d(
static_cast<int>(size),
cache.in,
cache.out,
FFTW_BACKWARD,
FFTW_ESTIMATE
);

if (!cache.forward_plan || !cache.inverse_plan) {
fftw_free(cache.in);
fftw_free(cache.out);
throw std::runtime_error("无法创建FFT计划");
}

plan_cache_[size] = cache;
}

std::vector<FFTUtils::Complex> FFTUtils::fft(const std::vector<double>& input) {
const size_t size = input.size();
if (size == 0) {
throw std::invalid_argument("输入向量不能为空");
}

PlanCache& cache = getPlanCache(size);

// 复制输入数据(实数转复数)
for (size_t i = 0; i < size; ++i) {
cache.in[i][0] = input[i]; // 实部
cache.in[i][1] = 0.0; // 虚部
}

// 执行FFT
fftw_execute(cache.forward_plan);

// 复制结果
std::vector<Complex> result(size);
for (size_t i = 0; i < size; ++i) {
result[i] = Complex(cache.out[i][0], cache.out[i][1]);
}

return result;
}

std::vector<FFTUtils::Complex> FFTUtils::fft(const std::vector<Complex>& input) {
const size_t size = input.size();
if (size == 0) {
throw std::invalid_argument("输入向量不能为空");
}

PlanCache& cache = getPlanCache(size);

// 复制输入数据
for (size_t i = 0; i < size; ++i) {
cache.in[i][0] = input[i].real(); // 实部
cache.in[i][1] = input[i].imag(); // 虚部
}

// 执行FFT
fftw_execute(cache.forward_plan);

// 复制结果
std::vector<Complex> result(size);
for (size_t i = 0; i < size; ++i) {
result[i] = Complex(cache.out[i][0], cache.out[i][1]);
}

return result;
}

std::vector<FFTUtils::Complex> FFTUtils::ifft(const std::vector<Complex>& input, bool normalize) {
const size_t size = input.size();
if (size == 0) {
throw std::invalid_argument("输入向量不能为空");
}

PlanCache& cache = getPlanCache(size);

// 复制输入数据
for (size_t i = 0; i < size; ++i) {
cache.in[i][0] = input[i].real(); // 实部
cache.in[i][1] = input[i].imag(); // 虚部
}

// 执行IFFT
fftw_execute(cache.inverse_plan);

// 复制结果,如需归一化则除以size
std::vector<Complex> result(size);
const double scale = normalize ? 1.0 / size : 1.0;

for (size_t i = 0; i < size; ++i) {
result[i] = Complex(
cache.out[i][0] * scale,
cache.out[i][1] * scale
);
}

return result;
}

std::vector<double> FFTUtils::magnitude(const std::vector<Complex>& input) {
std::vector<double> result(input.size());
for (size_t i = 0; i < input.size(); ++i) {
result[i] = std::abs(input[i]);
}
return result;
}

std::vector<double> FFTUtils::phase(const std::vector<Complex>& input, double epsilon) {
std::vector<double> result(input.size());
for (size_t i = 0; i < input.size(); ++i) {
// 使用atan2计算相位,避免除零问题
result[i] = std::atan2(input[i].imag(), input[i].real() + epsilon);
}
return result;
}

void FFTUtils::clearCache() {
std::lock_guard<std::mutex> lock(mutex_);

// 销毁所有计划并释放内存
for (auto& entry : plan_cache_) {
PlanCache& cache = entry.second;
if (cache.forward_plan) {
fftw_destroy_plan(cache.forward_plan);
}
if (cache.inverse_plan) {
fftw_destroy_plan(cache.inverse_plan);
}
if (cache.in) {
fftw_free(cache.in);
}
if (cache.out) {
fftw_free(cache.out);
}
}

plan_cache_.clear();
}

加入测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool test_fft(size_t size, double epsilon) {
try {
std::cout << "===== start FFT test :" << size << " =====" << std::endl;
// 1. 创建测试信号:前半部分为1.0,后半部分为0.0
std::vector<double> input(size, 0.0);
for (size_t i = 0; i < size / 2; ++i) {
input[i] = 1.0;
}

std::cout << "before" << std::min(size, (size_t)5) << "points: ";
for (size_t i = 0; i < std::min(size, (size_t)5); ++i) {
std::cout << input[i] << " ";
}
if (size > 5) std::cout << "...";
std::cout << std::endl;

// 2. 执行正向FFT
auto fft_result = FFTUtils::fft(input);

std::cout << "after FFT" << std::min(size, (size_t)5) << "points: ";
for (size_t i = 0; i < std::min(size, (size_t)5); ++i) {
std::cout << "(" << std::fixed << std::setprecision(2)
<< fft_result[i].real() << ","
<< fft_result[i].imag() << ") ";
}
if (size > 5) std::cout << "...";
std::cout << std::endl;
}
catch (const std::exception& e) {
std::cerr << "error: " << e.what() << std::endl;
return false;
}
}

以上fft代码均由豆包生成。生成dll,并在客户端调用这个函数,发现成功运行。

fft调用

参考文档

  1. C++ 日志库 Spdlog 报错: “Unicode support requires compiling with /utf-8“

  2. 【C++】spdlog光速入门,C++logger最简单最快的库