1、背景
spdlog是一个功能强大、易于使用和扩展的C++日志库,既可以以header-only方式引入到项目中,也支持预编译成 静态库或者动态库的方式引入到项目中。 spdlog目前有诸多版本:github.com/gabime/spdl... ,我们的项目:XFT中目前引入spdlog作为XFTLogger的实现基础,但假如上层的客户的项目也把spdlog作为其日志库来使用。
这就有概率导致 XFT使用的spdlog版本与客户服务代码使用的spdlog 版本不一致 的情况,由此可能会产生冲突或者不兼容的问题。
这相当于是两个有依赖关系的库A和B,同时都引入了另一个依赖库C,且依赖的C库的版本不同。
为了探究哪些情况下会产生冲突问题,哪些方式引入spdlog可以避免版本不一致导致的冲突,进行了以下多种case的实验。
2、实验情况
2.1 spdlog对比的版本
vivo的FTServing服务使用的是 spdlog-1.8.5,我们选取1.9.2 和 1.12.0 版本的spdlog作为实验测试对象。
- spdlog-1.8.5 vs 1.9.2: 主要接口,如 stdcout_color_sink_mt、basic_file_sink_mt、rotating_file_sink_mt 等接口完全一致,只不过部分接口的实现代码略有改动。
- spdlog-1.8.5 vs 1.12.0: 部分常用的接口发生变动,比如 basic_file_sink_mt、rotating_file_sink_mt。
c++
// spdlog-1.8.5 basic_file_sink的构造函数
template<typename Mutex>
class basic_file_sink final : public base_sink<Mutex>
{
public:
explicit basic_file_sink(const filename_t &filename, bool truncate = false);
// .......
}
// spdlog-1.12.0 basic_file_sink的构造函数
template<typename Mutex>
class basic_file_sink final : public base_sink<Mutex>
{
public:
explicit basic_file_sink(const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {});
// ........
}
2.2 项目结构
- xft_logger文件夹下的代码文件,使用Header-Only方式引入spdlog-1.8.5版本,编译成 xftlogger.so
- main.cpp 使用spdlog-1.12.0 或者 1.9.2版本,编译成可执行文件, xftlogger.so 会作为其依赖库。
xft_logger/log.h 内容:
c++
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <iosfwd>
#include <string.h>
#include <vector>
#include <array>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/stdout_sinks.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include <toml.hpp>
class xftLogger {
public:
// 构造函数
xftLogger();
// 析构函数, spdlog会管理其logger的生命周期.
~xftLogger() = default;
static xftLogger& instance() {
static xftLogger logger;
return logger;
}
std::shared_ptr<spdlog::logger> const Get() const noexcept {
return logger_;//.get();
}
// void no_destroy();
private:
// 指向spdlog::logger的智能指针
std::shared_ptr<spdlog::logger> logger_;
};
static xftLogger& logger = xftLogger::instance();
#define XFTLOG_FUNCTION static_cast<const char *>(__FUNCTION__)
#define XFTLOG_ERROR(...) logger.Get()->log(spdlog::source_loc{__FILE__, __LINE__, XFTLOG_FUNCTION}, spdlog::level::err, __VA_ARGS__)
xft_logger/log.cpp 内容:
c++
#include "log.h"
xftLogger::xftLogger() {
std::cout << "xftLogger::default Constructor" << std::endl;
// spdlog::warn("XFT_LOG_CONF::{} is not set or file does not exist, Use default config instead.",conf_file_path);
logger_ = std::make_shared<spdlog::logger>("TestXftLogger_01");
auto sink_1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
sink_1->set_level(spdlog::level::trace);
sink_1->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%n%$] [%l] %v");
logger_->sinks().push_back(sink_1);
// 🎭 basic_file_sink 🛑
auto sink_2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/xft2.log");
sink_2->set_level(spdlog::level::debug);
sink_2->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%-6l] %v");
logger_->sinks().push_back(sink_2);
logger_->set_level(spdlog::level::trace);
// logger_ = spdlog::basic_logger_mt("default_xft_logger", "logs/xft.log");
std::cout << "logger_->name() is " << logger_->name() << std::endl;
}
main.cpp 内容:
c++
#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <vector>
#include <array>
#include <set>
#include <algorithm>
#include "log.h"
#include "spdlog/spdlog.h"
#include "spdlog/details/file_helper.h"
int main() {
spdlog::info("Welcome to spdlog!");
auto logger00 = spdlog::logger("logger-00");
// spdlog::file_event_handlers handlers;
// handlers.before_open = [](spdlog::filename_t filename) { spdlog::info("Before opening {}", filename); };
// handlers.after_open = [](spdlog::filename_t filename, std::FILE *fstream) { fputs("After opening\n", fstream); };
// handlers.before_close = [](spdlog::filename_t filename, std::FILE *fstream) { fputs("Before closing\n", fstream); };
// handlers.after_close = [](spdlog::filename_t filename) { spdlog::info("After closing {}", filename); };
auto sink_1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto sink_2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/basic-log.txt", true); //, handlers);
sink_1->set_level(spdlog::level::info);
sink_1->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] %v");
logger00.sinks().push_back(sink_1);
logger00.info("Hello, World!@@@@");
XFTLOG_ERROR("XFTLOG_ERROR");
return 0;
}
CMakelists.txt
bash
cmake_minimum_required(VERSION 3.10)
set(CMAKE_C_COMPILER "/opt/compiler/gcc-8.2/bin/gcc")
set(CMAKE_CXX_COMPILER "/opt/compiler/gcc-8.2/bin/g++")
project(demoo)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE Debug)
set(XFTLoggerDir ${CMAKE_CURRENT_SOURCE_DIR}/xft_logger)
message(STATUS "===>> XFT Logger dir is ${XFTLoggerDir}")
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/toml11/)
set(xft_logger_src ${XFTLoggerDir}/log.cpp ${XFTLoggerDir}/log.h)
add_library(xftlogger SHARED ${xft_logger_src})
## target_compile_definitions(xftlogger PRIVATE SPDLOG_COMPILED_LIB)
###%%% 包含头文件路径时,属性设置为PRIVATE或者PUBLIC表示,是否把target引入的头文件搜索路径传递给此库的调用者(链接者)。
target_include_directories(xftlogger PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/include/)
# target_link_libraries(xftlogger PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/build/libspdlog.a)
# target_link_libraries(xftlogger INTERFACE /docker_workspace/personal/3rd_party/spdlog-1.12.0/build/libspdlog.a)
find_package(Threads)
### 全局引入头文件搜索路径
# include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/include/)
# include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.9.2/include)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.12.0/include/)
set(DEMO_SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
add_executable(demoo ${DEMO_SRC_LIST})
target_include_directories(demoo PRIVATE ${XFTLoggerDir})
target_link_libraries (${PROJECT_NAME} PRIVATE pthread xftlogger)
2.3 实验内容和结果
xftlogger.so 始终使用1.8.5-spdlog保持不变; main.cpp使用不同版本的spdlog;
1.8.5和1.12.0之间发生变动的接口有 basic_sink_file_mt , 此接口在xftLogger的构造函数中被调用。
2.3.1 实验1:调用spdlog位置与版本的影响
xftLogger 构造函数定义的位置 (内部调用了spdlog-1.8.5的接口) | main.cpp引入的spdlog版本 | 是否可以稳定的正常执行 |
---|---|---|
log.h | 1.9.2 | ✅ |
log.h | 1.12.0 | ❌ |
log.cpp | 1.9.2 | ✅ |
log.cpp | 1.12.0 | ❌ |
- 在main.cpp 引入spdlog-1.12.0时,无论xftLogger的构造函数在log.h 还是log.cpp,都不可以稳定正常执行,表现是:: 重新编译并运行多次,可能一部分时候,正常执行完毕,另一部分则会core dump。
分析可能的原因是 两个不同版本的 spdlog库定义了相同的接口,但实现不同,那么在链接时可能会出现符号冲突。不确定编译器会选择哪个版本的实现,导致出现偶发性的core dump。
- 在main.cpp 引入spdlog-1.9.2 时, 多次重新编译执行,均可以稳定正常执行完毕。
2.3.2 实验2:CMake中 targer_xxx_yyy中使用PUBLIC / PRIVATE的影响。
上述CMakelists.txt 在生成 libxftlogger.so时,使用的是 PRIVATE 属性,也就是 对内公开,对外不公开,不向 可执行文件 传递头文件目录以及链接库。
bash
target_include_directories(xftlogger PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/include/)
修改main.cpp, 使其调用spdlog-1.12.0 新修改的接口:
c++
#include "log.h"
#include "spdlog/spdlog.h"
#include "spdlog/details/file_helper.h"
int main() {
spdlog::info("Welcome to spdlog!");
auto logger00 = spdlog::logger("logger-00");
// 🔰 1.12.0版本新增, 1.8.5与1.9.2都没有。
spdlog::file_event_handlers handlers;
handlers.before_open = [](spdlog::filename_t filename) { spdlog::info("Before opening {}", filename); };
handlers.after_open = [](spdlog::filename_t filename, std::FILE *fstream) { fputs("After opening\n", fstream); };
auto sink_1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
// 🐝 1.12.0-spdlog的 basic_file_sink_mt接口才开始接受3个参数,1.8.5和1.9.2只有2个参数
auto sink_2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/basic-log.txt", true, handlers);
sink_1->set_level(spdlog::level::info);
sink_1->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] %v");
logger00.sinks().push_back(sink_1);
logger00.info("Hello, World!@@@@");
XFTLOG_ERROR("XFTLOG_ERROR");
return 0;
}
测试以下几种组合:
编译xftlogger.so时 target_include_directorie s使用的target属性 | 编译可执行文件时,通过include_directories 指定的spdlog版本 | 是否稳定正常执行 | 原因 |
---|---|---|---|
PRIVATE | 1.8.5 / 1.9.2 | ❌ | 1.8.5 / 1.9.2没有新接口的声明 |
PRIVATE | 1.12.0 | ❌ | 现象同2.3.1的现象,存在同名且定义不同的接口实现, 链接器或者可执行文件存不确定会执行哪一个。 |
PUBLIC | 1.8.5 / 1.9.2 / | ❌ | 1.8.5 / 1.9.2没有新接口的声明 |
PUBLIC | 1.12.0 | ❌ | 现象同2.3.1的现象,存在同名且定义不同的接口实现, 链接器或者可执行文件存不确定会执行哪一个。 |
表中的第4种case与第2中case相比, 都是通过 include_directories(spdlog-1.12.0) 全局包含 spdlog-1.12.0的头文件搜索路径, 唯一区别是,编译 xftlogger.so 时target_include_directories使用的target属性,一个是PUBLIC,一个是PRIVATE。
通过对比这两种 属性 CMAKE为demoo生成的 flags.make文件,发现:唯一区别就是 PUBLIC会把 xftlogger.so的搜索路径也加进来,但是放在spdlog-1.12.0的后面搜索,所以其实也没影响。
3、解决方法
Header-only 库的特点是它们的所有实现都包含在头文件中,这意味着每次包含这些头文件时,相关的代码都会被包含进编译单元中。
下面是可能的解决方法:
- 版本统一:确保XFT与客户的框架或者服务代码使用相同版本的spdlog库。 但XFT作为一个相对偏底层的算子库,不同的上层客户使用的spdlog可能也大相径庭,很难做到版本统一。
- 符号隐藏 :通过GCC编译器提供的编译选项 -fvisibility=hidden 与 代码内 attribute (((visibility("default"))) 组合使用,来控制xftLogger.so中spdlog-1.8.5的符号被隐藏,只暴露 xftLogger类的相关符号。参考:gcc.gnu.org/wiki/Visibi...
- 强制内联:spdlog库的接口前大都添加了 SPDLOG_INLINE 的宏(在GCC编译器下,展开为 inline ),inline有2种作用:
1)指令或者代码替换。但对Modern C++来说, inline只是对编译器的建议,编译器有自己的想法。
2)使得在多个翻译单元(Translation Unit, 可以理解为 .cc/.cpp等源文件) 定义同名同参函数成为了可能。而对于具有关键字inline的函数声明或者定义,链接器在链接阶段,一但发现具有多个定义的inline函数,其只取一个,因此,对于同名同参的inline函数,如果其实现不同,则会引起未定义行为(链接器只取其中一个,具体规则依赖于编译器)。
强制内联是通过添加编译选项 或者 在希望强制内联的接口声明定义前添加GCC编译器修饰符: attribute((always_inline)) 强制其进行指令或者代码替换,从而避免 符号冲突。
但这种方法需要修改源码,另外可能一些有符号冲突但却不适合进行 内联的实现可能 也会被进行内联。
- 命名空间:在spdlog源码中嵌套或者修改namespace, 但这涉及到对引入的开源第3方库源码的改动,不利于依赖库管理。
- Pimpl 惯用法:使用"Pointer to Implementation"(Pimpl)惯用法来隐藏库的实现细节,从而减少头文件的依赖和潜在的二进制不兼容问题。