记一次依赖库版本冲突:不同spdlog之间的兼容性问题

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.sotarget_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)惯用法来隐藏库的实现细节,从而减少头文件的依赖和潜在的二进制不兼容问题。
相关推荐
机器视觉知识推荐、就业指导几秒前
Qt/C++事件过滤器与控件响应重写的使用、场景的不同
开发语言·数据库·c++·qt
孤寂大仙v8 分钟前
【C++】STL----list常见用法
开发语言·c++·list
咩咩大主教1 小时前
C++基于select和epoll的TCP服务器
linux·服务器·c语言·开发语言·c++·tcp/ip·io多路复用
Ylucius3 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
是店小二呀3 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
ephemerals__3 小时前
【c++】动态内存管理
开发语言·c++
CVer儿3 小时前
条件编译代码记录
开发语言·c++
程序猿练习生4 小时前
C++速通LeetCode简单第18题-杨辉三角(全网唯一递归法)
c++·算法·leetcode
汉字萌萌哒4 小时前
【2022 CCF 非专业级别软件能力认证第一轮(CSP-J1)入门级 C++语言试题及解析】
数据结构·c++·算法
th新港4 小时前
CCF201909_1
数据结构·c++·算法·ccf