sylar高性能服务器-日志(P1-P6)代码解析+调试分析

​ 本篇文章主要针对一下sylar高性能服务器项目视频p1-p6的代码分析以及调试,大佬在讲课时基本上都是码代码,很少去详细的讲解代码的细节。像我这种没接触过服务器项目的小白看起来都很有压力,还别说能运行。下面的内容一到六是我自己对当前视频代码的一个简单分析,可能很多地方理解的不对,大家自己取舍。第七部分则是使用gdb进行调试,观察给出的示例是如何运行的,不然照着抄一遍基本看不懂写的什么,如果你写完代码甚至不知道每个模块具体做了什么,推荐可以参考我调试的步骤,没接触过gdb也无妨,只需要知道几个简单命令就行。
​ 最后我在附录中留了前面6节视频完整代码,在保证环境配置正确下可以完美进行调试。

项目视频链接

sylar

一、整体结构

日志模块类似于Log4j,目前的结构如下:

|–LogEvent
|–LogLevel
|–LogFormatter
|–LogAppender
|–StdoutAppender
|–FileLogAppender
|–Logger

  • LogEvent:日志的详细描述,包括文件名、线程ID、程序启动时间等一系列的信息
  • LogLevel:定义日志的级别
  • LogFormatter:定义日志的格式化输出
  • LogAppender:控制日志的输出地
    1. StdoutAppender:将日志输出到控制台
    2. FileLogAppender:将日志输出保存到文件
  • Logger:日志器,可以添加多个LogAppender和LogFormatter对象将其输出到多个地方

这里引用一下博主[令人头疼的编程]画的图,非常直观。

在这里插入图片描述

sylar在写代码时大量用到了智能指针,同样引用一下上面博主画的类之间的指针结构图,看代码时参照会轻松很多。

img

二、LogEvent

下列是目前定义的日志事件所包含的一些内容,可以看到sylar在定义变量时非常规范,常量、变量的范围、有符号或无符号都分得很清楚。

const char* m_file = nullptr;   // 文件名
int32_t m_line = 0;             // 行号,引入头文件stdint.h
uint32_t m_eplase = 0;          // 程序启动到现在的毫秒
uint32_t m_threadId = 0;        // 线程ID
uint32_t m_fiberId = 0;         // 协程ID
uint64_t m_time;                // 时间戳
std::stringstream m_ss;         // 内容

为了防止不同编译器中int所占的大小不同,在定义这些变量时基本使用重定义模式,而对于一些不应该为负数的变量,我们应该定义为无符号数,减少程序的漏洞。

sylar在定义日志内容时使用了stringstream而不是string,这是因为对于日志内容的数据可能经常需要进行类型转换,使用stringstream会更加的方便和安全

LogEvent的函数就比较简单,包括一个构造函数以及其它成员变量的取值函数。

// log.h
typedef std::shared_ptr<LogEvent> ptr; // [智能指针]
LogEvent(const char* file, int32_t line, uint32_t eplase
, uint32_t threadId, uint32_t fiberId, uint64_t time);

const char* getFile() const { return m_file;}
int32_t getLine() const { return m_line; }
uint32_t getEplase() const { return m_eplase; }
uint32_t getThreadId() const { return m_threadId; }
uint32_t getFiberId() const { return m_threadId; }
uint64_t getTime() const { return m_time; }
const std::string getcContent() const { return m_ss.str(); }
std::stringstream& getSS() { return m_ss; }

// log.cc
LogEvent::LogEvent(const char* file, int32_t line, uint32_t eplase
            , uint32_t threadId, uint32_t fiberId, uint64_t time) 
            :m_file(file),
            m_line(line),
            m_eplase(eplase),
            m_threadId(threadId),
            m_fiberId(fiberId),
            m_time(time) {

}

三、LogLevel

定义日志的级别,方便后期对不同的日志进行分类或者过滤接收某些日志。

class LogLevel {
public:
    enum Level{
        UNKNOW = 0,     //  未知 级别
        DEBUG = 1,      //  DEBUG 级别
        INFO = 2,       //  INFO 级别
        WARN = 3,       //  WARN 级别
        ERROR = 4,      //  ERROR 级别
        FATAL = 5       //  FATAL 级别
    };

    static const char* ToString(LogLevel::Level level);
};

ToString提供从日志级别到文本的转换,sylar使用了宏函数简化了繁琐的switch/case操作,以前看primer时也了解过宏函数的使用,不过属于看了就扔一边,平时写代码可以说基本没用过,第一次在真实项目中看到比较震撼,以后可以学着多用。后面代码中也有非常多的宏函数使用。

const char* LogLevel::ToString(LogLevel::Level level) {
    switch(level) { // [宏函数的使用]
#define XX(name) \
        case LogLevel::name: \
            return #name; \
            break;
    XX(DEBUG);
    XX(INFO);
    XX(WARN);
    XX(ERROR);
    XX(FATAL);
#undef XX
    default:
        return "UNKONW";
    }
    return "UNKONW";
}

四、LogFormatter

FormatItem作为LogFormatter的公共内部类成员,将日志内容格式化。

class LogFormatter {
public:
    typedef std::shared_ptr<LogFormatter> ptr;
    LogFormatter(const std::string& pattern);
    std::string format(std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event);
public:
    class FormatItem {                          // [类成员]
    public:
        typedef std::shared_ptr<FormatItem> ptr;
        virtual ~FormatItem() {}
        virtual void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) = 0;
    };

    void init(); 
private:
    std::string m_pattern;                      // 解析格式
    std::vector<FormatItem::ptr> m_items;       // 解析内容
};

格式化日志到流

不同类别的format继承于FormatItem,并重写format函数。

class MessageFormatItem : public LogFormatter::FormatItem { // 消息format
public:
    MessageFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getcContent();
    }
};

class LevelFormatItem : public LogFormatter::FormatItem { // 级别format
public:
    LevelFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << LogLevel::ToString(level);
    }
};

class LineFormatItem : public LogFormatter::FormatItem { 
public:
    LineFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getLine();
    }
};

class StringFormatItem : public LogFormatter::FormatItem { 
public:
    StringFormatItem(const std::string& str) 
        :m_string(str) {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << m_string;
    }
private:
    std::string m_string;
};

class EplaseFormatItem : public LogFormatter::FormatItem { 
public:
    EplaseFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getEplase();
    }
};

class LoggerNameFormatItem : public LogFormatter::FormatItem { 
public:
    LoggerNameFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << logger->getName();
    }
};

class ThreadIdFormatItem : public LogFormatter::FormatItem { 
public:
    ThreadIdFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getThreadId();
    }
};

class FiberIdFormatItem : public LogFormatter::FormatItem { 
public:
    FiberIdFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getFiberId();
    }
};

class DateTimeFormatItem : public LogFormatter::FormatItem { 
public:
    DateTimeFormatItem(const std::string& format = "%Y:%m:%d %H:%M:%S") 
        : m_format(format) {
    }
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getTime();
    }
private:
    std::string m_format;
};

init

这个函数应该是前6节视频中最复杂的

  1. 首先创建一个存储三元组的容器vec,用于
  2. 第一个for循环
    • m_pattern是一个字符串,表示传入的格式模板,比如%Y:%m:%d %H:%M:%S,依次循环取m_pattern中的字符,分别判断普通字符、%、{}、空格等
    • (A) 代码对应有标记,给出了注释。
    • 进入到判断(B),说明当前字符是%,如果后面还有字符并且也是%,则不需要对当前的%作任何处理,进而去判断下一个%
    • 进入©,说明正好获取到了一个%,并且后面的字符不是%,那么就可以进行分析,这里定于了几个变量,直接看代码注释。
    • 进入(D)是一个while循环,从当前字符的下一个元素n开始,如果下标为n的字符不是字母,或者{},直接退出循环。如果fmt_status为0,表示还没有遇到过{,进入语句,如果当前字符为{,则截取一段字符串,范围为(i + 1, n - i - 1),i是进入(D)之前的左边界下标,n是在while循环里变化的右边界下标,每当遇到一个{,就会把它左边的字符全部放入内容字符串str中,然后把fmt_status赋值1,fmt_begin赋值为n,记录下一个开始的位置。
    • 如果fmt_status为1,并且遇到了},同样也是截取一段字符串,赋值给fmt,这时fmt就表示大括号里面的字符,最后把状态置为2,表示处理完了括号里面的数据。
    • 整个while循环的目的就是为了解析时间格式,我们传入的模板比如是{%d{%Y-%m-%d …}},经过这段代码,str = d,表示正在解析的是时间,fmt就等于解析后的时间格式%Y-%m-%d ...
    • 进入(E),退出了while循环,如果状态为0,表示要解析后面的字符,那么就要先把之前解析的内容存入vec容器中;如果状态为1,说明传入的格式模板不正确,输出错误提示,并往vec中写入一个错误信息;如果状态为2,这里感觉代码重复了,后面sylar应该会修改。
  3. 进入(F),m_pattern的格式模板已经全部解析完成,定义一个map,第一个参数为相应的日志格式,第二个参数为FormatItem智能指针。里面存放不同类型的FormatItem,比如MessageFormatItem、LevelFormatItem,前面都有相应的定义。然后变量vec,先判断三元组第二个参数是否等于0,如果是说明这一条元组是一个类型字符代号(%d就是时间),存入到m_items中,然后调用相应的format方法。如果不是0,表示相应字符代号后面的内容,则去创建的map里面遍历,找到了就push到m_items,否则push一条错误信息。
void LogFormatter::init() {
    std::vector<std::tuple<std::string, std::string,int>> vec;  // [tuple]  str:内容,format:格式模板, type:0表示格式模板中[]里面的字符代号,比如时间d,消息m,1表示代号跟着的内容
    std::string nstr; // 结果字符串
    for(size_t i = 0; i < m_pattern.size(); ++ i) {
        if(m_pattern[i] != '%') {		// (A)如果当前字符不是%,直接加入结果字符串nstr
            nstr.append(1,m_pattern[i]); // [append]
            continue;
        }

        if((i + 1) < m_pattern.size()) { // (B)
            if(m_pattern[i + 1] == '%') {
                nstr.append(1, '%');
                continue;
            }
        }

        size_t n = i + 1; 	    // (C) n表示后一个字符的位置
        int fmt_status = 0;     // 定义个状态初始为0 
        std::string str;        // 内容字符串,也就是传入格式模板的%m
        std::string fmt;        // 用于寻找括号
        size_t fmt_begin = 0;   //  记录当前下标的位置

        while(n < m_pattern.size()) { // (D)
            if(!isalpha(m_pattern[n]) && m_pattern[n] != '{' && m_pattern[n] != '}') { // 判断当前字符是否维空格
                break;
            }
            if(fmt_status == 0) {	
                if(m_pattern[n] == '{') { // 解析时间格式
                    str = m_pattern.substr(i + 1, n - i - 1); // str = d
                    fmt_status = 1; // 解析格式
                    fmt_begin = n;
                    ++ n;
                    continue;
                }
            }
            if(fmt_status == 1) {
                if(m_pattern[i] == '}') { // 结束解析时间格式
                    fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1); // fmt是时间格式,比如%Y-%m-%d之类
                    fmt_status = 2;
                    break;
                }
            }

            ++ n;

        }
        if(fmt_status == 0) {  // (E)
            if(!nstr.empty()) {
                vec.push_back(std::make_tuple(nstr, std::string(), 0));
                nstr.clear();
            }
            str = m_pattern.substr(i + 1, n - i - 1);
            vec.push_back(std::make_tuple(str, fmt, 1));
            i = n - 1;
        } else if(fmt_status == 1) {
            std::cout << "pattern parse error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;
            vec.push_back(std::make_tuple("<parse_error>", fmt, 0));
        } else if(fmt_status == 2) {
            if(!nstr.empty()) {
                vec.push_back(std::make_tuple(nstr, std::string(), 0));
                nstr.clear();
            }
            vec.push_back(std::make_tuple(str, fmt, 1));
            i = n - 1;
        }
            
    }
    if(!nstr.empty()) {
        vec.push_back(std::make_tuple(nstr, std::string(), 0));

    }
    // [function] 引入function (F)
    static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)>> s_format_items = {
        // {"m",[](const std::string& fmt) { return FormatItem::ptr(new MessageFormatItem(fmt)); } }
#define XX(str,C) \
        {#str, [] (const std::string& fmt) { return FormatItem::ptr(new C(fmt)); }}

        XX(m, MessageFormatItem),           //m:消息
        XX(p, LevelFormatItem),             //p:日志级别
        XX(r, EplaseFormatItem),            //r:累计毫秒数
        XX(c, LoggerNameFormatItem),              //c:日志名称
        XX(t, ThreadIdFormatItem),          //t:线程id
        XX(n, LineFormatItem),           //n:换行
        XX(d, DateTimeFormatItem),          //d:时间
        XX(l, LineFormatItem),              //l:行号
        XX(F, FiberIdFormatItem),           //F:协程id
#undef XX
    };
    for(auto& i : vec) {
        if(std::get<2>(i) == 0) { // [?猜测是三元组下标取值]
            m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));
        } else {
            auto it = s_format_items.find(std::get<0>(i));
            if(it == s_format_items.end()) {
                m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));
            } else {
                m_items.push_back(it->second(std::get<1>(i)));
            }
        }
        std::cout << '(' << std::get<0>(i) << ") - (" << std::get<1>(i) << ") - (" << std::get<2>(i) << ')' << std::endl;
    }
}

五、LogAppender

LogAppender定义将日志输出的目的地,包含两个子类StdoutAppender、FileLogAppender,分别将日志输出到控制台和文件,后期也可以增加其它的一些输出地。

成员函数包括一个日志级别m_level和输出的格式器m_formatter。

setFormatter用于定义日志输出使用的格式化方法。getFormatter获得格式化方法。

class LogAppender {
public:
    typedef std::shared_ptr<LogAppender> ptr;
    virtual ~LogAppender() {}                                           // 为了便于该类的派生类调用,定义为[虚类],

    virtual void log(std::shared_ptr<Logger> logger,LogLevel::Level level, LogEvent::ptr event) = 0;   // [纯虚函数],子类必须重写

    void setFormatter(LogFormatter::ptr val) { m_formatter = val; }
    LogFormatter::ptr getFormatter() const { return m_formatter; }
protected:
    LogLevel::Level m_level = LogLevel::DEBUG;                                            // 级别,为了便于子类访问该变量,设置在保护视图下
    LogFormatter::ptr m_formatter;                                      // 定义输出格式
};

输出到控制台

m_formatter时一个vector,里面包含多个不同类型格式化,调用format函数时会依次遍历输出到流。

这里的条件语句应该是根据日志级别进行一个简单的过滤

// log.h
class StdoutAppender : public LogAppender {
public:
    typedef std::shared_ptr<StdoutAppender> ptr; 
    void log(Logger::ptr logger,LogLevel::Level level, LogEvent::ptr event) override;
private:
};

// log.cc
void StdoutAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
    if(level >= m_level) {
        std::cout << m_formatter->format(logger,level,event); // namespace "std" has no member "cout" 加入iostream
    }
}

输出到文件

成员包括文件名m_filename和文件输出流m_filestream。

reopen函数用于打开一个文件,最后返回应该是判断文件打开是否成功,不知道这里用两个!是什么意思

log函数用于输出到文件

// log.h
class FileLogAppender : public LogAppender {
public:
    typedef std::shared_ptr<FileLogAppender> ptr;
    FileLogAppender(const std::string& filename);                   // 输出的文件名
    void log(Logger::ptr logger,LogLevel::Level level, LogEvent::ptr event) override;  // [override]

    bool reopen();                                                  // 重新打开文件,成功返回true
private:
    std::string m_filename;
    std::ofstream m_filestream;                                     // 引入sstream

};
// log.cc
bool FileLogAppender::reopen() {
    if(m_filestream) {
        m_filestream.close();
    }

    m_filestream.open(m_filename);
    return !!m_filestream; // [?]
}
void FileLogAppender::log(std::shared_ptr<Logger> logger,LogLevel::Level level, LogEvent::ptr event) {
    if(level >= m_level) {
        m_filestream << m_formatter->format(logger,level,event);
    }
}

六、Logger

成员函数包含日志名称m_name、日志级别m_level、日志输出地集合m_appender、格式器m_formatter。

构造函数进行一个简单初始化日志名、日志级别以及默认日志格式

log函数会遍历m_appender里面的日志输出地,调用对应的Appender的log函数进行输出。

addAppender函数会在当前日志item没有格式器时赋予一个默认值,否则直接加入m_appender集合

delAppender函数删除一个指定的日志输出地

// log.h
class Logger : public std::enable_shared_from_this<Logger>{ // <?>为了传递智能指针参数
public:
    typedef std::shared_ptr<Logger> ptr;
    
    Logger(const std::string& name = "root");

    void log(LogLevel::Level level, LogEvent::ptr event);

    // 不同级别的日志输出函数
    void debug(LogEvent::ptr event);
    void info(LogEvent::ptr event);
    void warn(LogEvent::ptr event);
    void fatal(LogEvent::ptr event);
    void error(LogEvent::ptr event);

    void addAppender(LogAppender::ptr appender);            // 添加一个appender
    void delAppender(LogAppender::ptr appender);            // 删除一个appender
    LogLevel::Level getLevel() const { return m_level; }    // [const放在函数后]
    void setLevel(LogLevel::Level val) { m_level = val; }   // 设置级别

    const std::string& getName() const { return m_name; }
private:
    std::string m_name;                                     // 日志名称
    LogLevel::Level m_level;                                // 级别
    std::list<LogAppender::ptr> m_appender;                 // Appender集合,引入list
    LogFormatter::ptr m_formatter;						  // 格式器
};
// log.cc
Logger::Logger(const std::string& name) 
        :m_name(name),
        m_level(LogLevel::DEBUG) {
    m_formatter.reset(new LogFormatter("%d [%p] <%f:%l> %m %n")); // 定义一个默认的日志格式
}
void Logger::log(LogLevel::Level level, LogEvent::ptr event)  {
    if(level >= m_level) {
        auto self = shared_from_this(); // <?> 获得logger的智能指针 
        for(auto &i : m_appender) {
            i->log(self,level, event);
        } 
    }
}
void Logger::addAppender(LogAppender::ptr appender) {
    if(!appender->getFormatter()) {
        appender->setFormatter(m_formatter); // 保证每一个日志都有默认格式
    }
    m_appender.push_back(appender);
}
void Logger::delAppender(LogAppender::ptr appender) {
    for(auto it = m_appender.begin(); it != m_appender.end(); ++ it) {
        if(*it == appender) {
            m_appender.erase(it);
            break;
        }
    }
}

七、调试

调试需要用到cmake一系列的工具,我是用的阿里云服务器,很多库都有了,如果没有可以参考下列网站去安装。

项目环境配置

我会把到P6为止的代码贴在文章最后,如果环境没问题是可以编译运行的。下面我也简单写一下自己调试的步骤(已安装必须环境)。

7.1调试步骤

  1. 在最外层sylar文件夹创建CMakeLists.txt,我的项目结构如下,红线划掉的是后面使用cmake命令生成的

    image-20231007150731552

    然后往CMakeLists.txt写入下列代码,不懂cmake的可以看看这篇文章:https://blog.csdn.net/weixin_43717839/article/details/128032486。

    cmake_minimum_required(VERSION 2.8)
    project(sylar)
    
    set(CMAKE_VERBOSE_MAKEFILE ON) 
    set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} -rdynamic -O0 -g -std=c++11 -Wall -Wno-deprecated -Werror -Wno-unused-function")
    
    set(LIB_SRC
        sylar/log.cc
        # sylar/util.cc
        )
    
    add_library(sylar SHARED ${LIB_SRC})
    #add_library(sylar_static STATIC ${LIB_SRC})
    #SET_TARGET_PROPERTIES(sylar_static PROPERTIES OUTPUT_NAME "sylar")
    
    add_executable(test tests/test.cc)
    add_dependencies(test sylar)
    target_link_libraries(test sylar)
    
    SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
    SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
    
    
  2. 在tests文件夹创建一个test.cc文件,写入下列代码

    #include <iostream>
    #include "../sylar/log.h"
    
    int main(int argc, char** argv) {
        sylar::Logger::ptr logger(new sylar::Logger);
        logger->addAppender(sylar::LogAppender::ptr(new sylar::StdoutAppender));
    
    
        sylar::LogEvent::ptr event(new sylar::LogEvent(__FILE__, __LINE__, 0, 1, 2, time(0)));
        // event->getSS() << "hello sylar log";
        logger->log(sylar::LogLevel::DEBUG, event);
        std::cout << "hello sylar log" << std::endl;
    
        return 0;
    }
    
  3. 然后在终端输入命令

    cmake .
    make
    
  4. 如果代码有问题,输入make后会报错,根据提示改就行。在能成功编译的情况下,会在bin文件夹生成一个可执行文件test

    image-20231007151246942

  5. 输入bin/test就可以运行,结果如下

    image-20231007151321030

7.2尝试使用gdb调试

参考视频以及别人的文章,一行一行把代码看了一遍,不过还是很蒙,所以打算调试一下看测试例子是如何一步一步解析的,正好学习一下gdb调试工具。

没有用过gdb调试可以看看这篇文章https://blog.csdn.net/chen1415886044/article/details/105094688?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169666829816800182168185%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=169666829816800182168185&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-105094688-null-null.142v95chatgptT3_1&utm_term=gdb%E8%B0%83%E8%AF%95&spm=1018.2226.3001.4187

image-20231007190539718

测试gdb是否能执行

  1. 进入文件夹bin,输入下列命令,出现Reading sy…就说明已经加载成功

    image-20231007190459565

  2. 输入命令run(简写r),因为程序没有断点,直接输出结果

    image-20231007191242125

开始调试

test.cc文件代码如下,

#include <iostream>
#include "../sylar/log.h"

int main(int argc, char** argv) {
    sylar::Logger::ptr logger(new sylar::Logger);
    logger->addAppender(sylar::LogAppender::ptr(new sylar::StdoutAppender));
    sylar::LogEvent::ptr event(new sylar::LogEvent(__FILE__, __LINE__, 0, 1, 2, time(0)));
    // event->getSS() << "hello sylar log";
    logger->log(sylar::LogLevel::DEBUG, event);
    std::cout << "hello sylar log" << std::endl;

    return 0;
}

调试一

首先对sylar::Logger::ptr logger(new sylar::Logger);使用gdb进行调试

  1. 进入gdb模式,并在第5行打上一个断点,然后启动
    image-20231007194913157

  2. 输入s进入函数内部,输入l查看当前代码,并在132行继续添加一个断点

    image-20231007195117777

  3. 这里是一个日志器logger的构造函数,输入n进行逐语句执行(当前代码已经执行到了129行),直到遇到刚刚在132行设置的断点,并查看logger4个成员变量的值。可以看到,日志名称和级别都是使用的默认值,而日志输入地appender和格式器formatter目前都还没有初始化

    image-20231007195550628

  4. 目前已经到了第二个断点,这句代码是初始化logger的格式器formatter,括号里新建了一个LogFormatter对象,输入s进入LogFormatter的构造函数。m_pattern = %d [%p] <%f:%l> %m %n

    image-20231007200550479

  5. 语句现在执行到了203行,init函数会将第二个断点处(132行)传入格式模板进行解析,所以在这里204行设置一个断点,输入s进入函数内部

    image-20231007201004757

  6. 在init函数304行会对三元组容器vec(解析模板后的内容)进行遍历打印,为了方便我们在这里也加入一个断点

    image-20231007201412398

  7. [%](现在每一个循环遍历的元素我都写在序号后)下面进入init函数的第一个for循环,查看每一次循环后三元组vec、字符标记(例如[d],[m]之类的,)str,字符标记后面的内容fmt、以及nstr。一直输入n直到一次循环结束。m_pattern = %d [%p] <%f:%l> %m %n,第一次循环解析的值为’%‘,那么跳过第一个if语句,进入第二个if语句,但是由于下一个字符不是’%‘,继续执行解析当前字符’%’

    image-20231007202333737

    [d]进入while循环,不满足里面的条件判断,n++

    [ ]遇到空格,触发第一个条件判断,跳出while循环,执行263行代码if(!nstr.empty())

    image-20231007204211547

    fmt_status = 0,表示没有遇到{}里面的内容,当前str = ’d’,nstr为空

    image-20231007204605833

    到这里第一次循环结束,vec里面有一个元组,解析到了格式模板里面的时间字符d

    image-20231007204920892

  8. [ ]上一个循环结尾i = n - 1 = 1,经历一个i++,i = 2所以当前元素是空格,进入第一个条件判断,把空格字符加入nstr,结束当前for循环

    image-20231007205328552

  9. [ [ ] i = 3遇到括号[, 继续加入nstr,然后结束当前循环,目前nstr = " ["

  10. [ % ] 遇到%,并且下一个不是%,说明下一个是一个字符标记(p,日志级别),进入while循环,和上面第一次for循环类似,就不详细说明,依次遍历p、]然后遇到一个空格跳出while循环。进入263行代码,目前nstr = " [",所以需要把nstr加入到vec中

    image-20231007210208506

    现在vec就有两条记录,刚加入的nstr第三个参数为0,可以理解为非字符标记,也就是实际内容。

    image-20231007210300121

    然后继续截取str = p,这是一个字符标记,同样也写入vec中

    image-20231007210420896

  11. ]继续循环,当前字符为],后面的循环依次把"] <"三个字符加入nstr中

    image-20231008083801978

  12. [ % ]遇到%,下一个字符为f,则说明遇到字符标记,把之前存入nstr的"] <"存入vec中

    image-20231008084422018

    然后从while出来后(遇到了空格跳出),str = f,将其存入vec

    image-20231008085248023

  13. 后面的for循环解析格式模板我就跳过了,大致就是遇到%就判断是否是字符标记,遇到空格就把之间 的nstr存入vec,然后再提取字符标记str,再存入vec;如果没有遇到%,就直接把当前元素存入nstr,直到下一次遇到%再做处理。结束for循环后,vec容器存储如下

    image-20231008085704942

  14. [“d 1”]现在进入断点304,也就是根据我们自定义的map,把vec容器的元素解析为不同的类型formatter(例如消息、时间、level等)。对于vec中的第一个元素“d 1",进入条件判断中的else部分

    image-20231008090208632

    在map中查找是否存储key为”d“的键值对,找到后实例化对应的DateTimeFormatItem,然后把指针存入到m_items中。遍历完vec容器,最后就得到了一个解析完格式模板后的一个指针容器m_items。

调试2

对语句logger->addAppender(sylar::LogAppender::ptr(new sylar::StdoutAppender));进行调试,回顾一下logger的初始化,我们主要是解析传入的默认格式模板,然后把结果存入指针容器m_items。下图就是vec容器的输出,那么m_items里就存放在例如MessageFormatItem等FormatItem

image-20231008092205735

logger的四个参数除了m_appender以外都进行了初始化,下面就是给logger添加一个日志输出地appender。

  1. 还是在添加appender的语句打一个断点。然后输入s进入函数内部,

    image-20231008092747950

    传入的参数是一个输出到控制台的appender,因此进入StdoutAppender类,进行初始化(使用默认构造函数)

    image-20231008092943563

    appender初始化完成后,调用logger的方法添加,并把之前解析出来的m_formatter存入到该appender中

    image-20231008094607241

调试3

针对语句sylar::LogEvent::ptr event(new sylar::LogEvent(__FILE__, __LINE__, 0, 1, 2, time(0)));

到此一个logger已经完完全全构建好了

  1. 在改语句打断点,进入到event的构造函数,传入的参数前两个因为目前还没有,传入占位符,后3个分别是程序启动的毫秒:0、线程ID:1、携程ID:2、内容默认为空。进入构造函数内部

    image-20231008095743832

调试4

针对语句logger->log(sylar::LogLevel::DEBUG, event);

  1. 在目标语句打上断点,进入logger的log函数

    image-20231008100128113

    image-20231008100135140

  2. 遍历m_appender容器,我们只有一个,所以进入相应appender的log函数

    image-20231008100252719

    在log函数里面调用m_formatter的format方法

    image-20231008100546737

    来到下面函数,这里代码是错误的,需要把return ss.str()放在for循环后,否则只能打印一个时间

    image-20231008100742808

  3. m_items存放的是之前解析的指向格式模板的指针

    image-20231008100851171

    第一个解析的是日期d,所以进入DateTimeFormatItem,调用里面的format进行输出,在时间的format方法里面,会获取当前时间戳,然后转换为本地时间,存入到流中

    image-20231008101432173

  4. 其它的也是相应的formatter进行打印,遍历完appender后,往控制台打印了解析的结果如下

    image-20231008103742014

八、附录

8.1log.h

#ifndef __SYLAR_LOG_H__
#define __SYLAR_LOG_H__

#include<string>
#include<stdint.h>
#include<memory>
#include<list>
#include<vector>
#include<sstream>
#include<fstream>

namespace sylar{ // 防止和别人的代码冲突

class Logger;  // <把Logger放到这里的目的?> 前面的一些类会用到Logger,不加会报未定义错误
// 日志的一些配置
class LogEvent {
public:
    typedef std::shared_ptr<LogEvent> ptr; // [智能指针]
    LogEvent(const char* file, int32_t line, uint32_t eplase
            , uint32_t threadId, uint32_t fiberId, uint64_t time);

    const char* getFile() const { return m_file;}
    int32_t getLine() const { return m_line; }
    uint32_t getEplase() const { return m_eplase; }
    uint32_t getThreadId() const { return m_threadId; }
    uint32_t getFiberId() const { return m_threadId; }
    uint64_t getTime() const { return m_time; }
    const std::string getcContent() const { return m_ss.str(); }
    std::stringstream& getSS() { return m_ss; }
private:
    const char* m_file = nullptr;   // 文件名
    int32_t m_line = 0;             // 行号,引入头文件stdint.h
    uint32_t m_eplase = 0;          // 程序启动到现在的毫秒
    uint32_t m_threadId = 0;         // 线程ID
    uint32_t m_fiberId = 0;          // 协程ID
    uint64_t m_time;                // 时间戳
    std::stringstream m_ss;          // 内容
};

// 自定义日志级别
class LogLevel {
public:
    enum Level{
        UNKNOW = 0,     //  未知 级别
        DEBUG = 1,      //  DEBUG 级别
        INFO = 2,       //  INFO 级别
        WARN = 3,       //  WARN 级别
        ERROR = 4,      //  ERROR 级别
        FATAL = 5       //  FATAL 级别
    };

    static const char* ToString(LogLevel::Level level);
};

// 日志格式
class LogFormatter {
public:
    typedef std::shared_ptr<LogFormatter> ptr;
    LogFormatter(const std::string& pattern);
    std::string format(std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event);
public:
    class FormatItem {                          // [类中类]
    public:
        typedef std::shared_ptr<FormatItem> ptr;
        virtual ~FormatItem() {}
        virtual void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) = 0;
    };

    void init(); 
private:
    std::string m_pattern;                      // 解析格式
    std::vector<FormatItem::ptr> m_items;       // 解析内容
};

// 日志输出地
class LogAppender {
public:
    typedef std::shared_ptr<LogAppender> ptr;
    virtual ~LogAppender() {}                                           // 为了便于该类的派生类调用,定义为[虚类],

    virtual void log(std::shared_ptr<Logger> logger,LogLevel::Level level, LogEvent::ptr event) = 0;   // [纯虚函数],子类必须重写

    void setFormatter(LogFormatter::ptr val) { m_formatter = val; }
    LogFormatter::ptr getFormatter() const { return m_formatter; }
protected:
    LogLevel::Level m_level = LogLevel::DEBUG;                                            // 级别,为了便于子类访问该变量,设置在保护视图下
    LogFormatter::ptr m_formatter;                                      // 定义输出格式
};

// 日志输出器
class Logger : public std::enable_shared_from_this<Logger>{ // [?]
public:
    typedef std::shared_ptr<Logger> ptr;
    
    Logger(const std::string& name = "root");

    void log(LogLevel::Level level, LogEvent::ptr event);

    // 不同级别的日志输出函数
    void debug(LogEvent::ptr event);
    void info(LogEvent::ptr event);
    void warn(LogEvent::ptr event);
    void fatal(LogEvent::ptr event);
    void error(LogEvent::ptr event);

    void addAppender(LogAppender::ptr appender);            // 添加一个appender
    void delAppender(LogAppender::ptr appender);            // 删除一个appender
    LogLevel::Level getLevel() const { return m_level; }    // [const放在函数后]
    void setLevel(LogLevel::Level val) { m_level = val; }   // 设置级别

    const std::string& getName() const { return m_name; }
private:
    std::string m_name;                                     // 日志名称
    LogLevel::Level m_level;                                // 级别
    std::list<LogAppender::ptr> m_appender;                 // Appender集合,引入list
    LogFormatter::ptr m_formatter;
};

// 输出方法分类

// 输出到控制台
class StdoutAppender : public LogAppender {
public:
    typedef std::shared_ptr<StdoutAppender> ptr; 
    void log(Logger::ptr logger,LogLevel::Level level, LogEvent::ptr event) override;
private:
};

// 输出到文件
class FileLogAppender : public LogAppender {
public:
    typedef std::shared_ptr<FileLogAppender> ptr;
    FileLogAppender(const std::string& filename);                   // 输出的文件名
    void log(Logger::ptr logger,LogLevel::Level level, LogEvent::ptr event) override;  // [override]

    bool reopen();                                                  // 重新打开文件,成功返回true
private:
    std::string m_filename;
    std::ofstream m_filestream;                                     // stringstream要报错,引入sstream

};
}

#endif

8.2log.cc

#include"log.h"
#include<iostream>
#include<map>
#include<functional>
#include<time.h>
#include<string.h>

namespace sylar {

const char* LogLevel::ToString(LogLevel::Level level) {
    switch(level) { // [宏函数的使用]
#define XX(name) \
        case LogLevel::name: \
            return #name; \
            break;
    XX(DEBUG);
    XX(INFO);
    XX(WARN);
    XX(ERROR);
    XX(FATAL);
#undef XX
    default:
        return "UNKONW";
    }
    return "UNKONW";
}


class MessageFormatItem : public LogFormatter::FormatItem { // [继承类中类]
public:
    MessageFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getcContent();
    }
};

class LevelFormatItem : public LogFormatter::FormatItem { 
public:
    LevelFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << LogLevel::ToString(level);
    }
};

class LineFormatItem : public LogFormatter::FormatItem { 
public:
    LineFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getLine();
    }
};

class StringFormatItem : public LogFormatter::FormatItem { 
public:
    StringFormatItem(const std::string& str) 
        :m_string(str) {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << m_string;
    }
private:
    std::string m_string;
};

class EplaseFormatItem : public LogFormatter::FormatItem { 
public:
    EplaseFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getEplase();
    }
};

class LoggerNameFormatItem : public LogFormatter::FormatItem { 
public:
    LoggerNameFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << logger->getName();
    }
};

class ThreadIdFormatItem : public LogFormatter::FormatItem { 
public:
    ThreadIdFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getThreadId();
    }
};

class FiberIdFormatItem : public LogFormatter::FormatItem { 
public:
    FiberIdFormatItem(const std::string& fmt = "") {}
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        os << event->getFiberId();
    }
};

class DateTimeFormatItem : public LogFormatter::FormatItem { 
public:
    DateTimeFormatItem(const std::string& format = "%Y:%m:%d %H:%M:%S") 
        : m_format(format) {
            if(m_format.empty()) {
                m_format = "%Y:%m:%d %H:%M:%S";
            }
    }
    void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
        // os << event->getTime();
        struct tm tm;
        time_t time = event->getTime(); // 当前时间戳 引入time.h
        localtime_r(&time, &tm); // 将时间戳转换为本地时间,并将结果存放在tm中
        char buf[64];
        strftime(buf, sizeof(buf), m_format.c_str(), &tm);
        os << buf;
    }
private:
    std::string m_format;
};

LogEvent::LogEvent(const char* file, int32_t line, uint32_t eplase
            , uint32_t threadId, uint32_t fiberId, uint64_t time) 
            :m_file(file),
            m_line(line),
            m_eplase(eplase),
            m_threadId(threadId),
            m_fiberId(fiberId),
            m_time(time) {

}


Logger::Logger(const std::string& name) 
        :m_name(name),
        m_level(LogLevel::DEBUG) {
    m_formatter.reset(new LogFormatter("%d [%p] <%f:%l> %m %n"));
}

void Logger::addAppender(LogAppender::ptr appender) {
    if(!appender->getFormatter()) {
        appender->setFormatter(m_formatter); // 保证每一个日志都有默认格式
    }
    m_appender.push_back(appender);
}           
void Logger::delAppender(LogAppender::ptr appender) {
    for(auto it = m_appender.begin(); it != m_appender.end(); ++ it) {
        if(*it == appender) {
            m_appender.erase(it);
            break;
        }
    }
}

void Logger::log(LogLevel::Level level, LogEvent::ptr event)  {
    if(level >= m_level) {
        auto self = shared_from_this(); // [?] 
        for(auto &i : m_appender) {
            i->log(self,level, event);
        } 
    }
}

void Logger::debug(LogEvent::ptr event) {
    log(LogLevel::DEBUG, event); 
}
void Logger::info(LogEvent::ptr event) {
    log(LogLevel::INFO, event); 
}
void Logger::warn(LogEvent::ptr event) {
    log(LogLevel::WARN, event); 
}
void Logger::fatal(LogEvent::ptr event) {
    log(LogLevel::ERROR, event); 
}
void Logger::error(LogEvent::ptr event) {
    log(LogLevel::FATAL, event); 
}


FileLogAppender::FileLogAppender(const std::string& filename) 
    : m_filename(filename) {

}

void FileLogAppender::log(std::shared_ptr<Logger> logger,LogLevel::Level level, LogEvent::ptr event) {
    if(level >= m_level) {
        m_filestream << m_formatter->format(logger,level,event);
    }
}

bool FileLogAppender::reopen() {
    if(m_filestream) {
        m_filestream.close();
    }

    m_filestream.open(m_filename);
    return !!m_filestream; // [?]
}

void StdoutAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
    if(level >= m_level) {
        std::cout << m_formatter->format(logger,level,event) << std::endl; // namespace "std" has no member "cout" 加入iostream
    }
}

LogFormatter::LogFormatter(const std::string& pattern) 
    : m_pattern(pattern) {
        init();
}

std::string LogFormatter::format(std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) {
    std::stringstream ss;
    for(auto &i : m_items) {
        i->format(ss,logger,level,event);
      
    }
    return ss.str();
}

// 日志格式定义
void LogFormatter::init() {
    std::vector<std::tuple<std::string, std::string,int>> vec;  // [tuple]  str,format, type
    std::string nstr; //当前str
    for(size_t i = 0; i < m_pattern.size(); ++ i) {
        if(m_pattern[i] != '%') {
            nstr.append(1,m_pattern[i]); // [append]
            continue;
        }

        if((i + 1) < m_pattern.size()) {
            if(m_pattern[i + 1] == '%') {
                nstr.append(1, '%');
                continue;
            }
        }

        size_t n = i + 1;
        int fmt_status = 0;
        std::string str;
        std::string fmt;
        size_t fmt_begin = 0; // 

        while(n < m_pattern.size()) {
            if(!isalpha(m_pattern[n]) && m_pattern[n] != '{' && m_pattern[n] != '}') { // 判断当前字符是否维空格
                break;
            }
            if(fmt_status == 0) {
                if(m_pattern[n] == '{') {
                    str = m_pattern.substr(i + 1, n - i - 1);
                    fmt_status = 1; // 解析格式
                    fmt_begin = n;
                    ++ n;
                    continue;
                }
            }
            if(fmt_status == 1) {
                if(m_pattern[i] == '}') {
                    fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1);
                    fmt_status = 2;
                    break;
                }
            }

            ++ n;

        }
        if(fmt_status == 0) {
            if(!nstr.empty()) {
                vec.push_back(std::make_tuple(nstr, std::string(), 0));
                nstr.clear();
            }
            str = m_pattern.substr(i + 1, n - i - 1);
            vec.push_back(std::make_tuple(str, fmt, 1));
            i = n - 1;
        } else if(fmt_status == 1) {
            std::cout << "pattern parse error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;
            vec.push_back(std::make_tuple("<parse_error>", fmt, 0));
        } else if(fmt_status == 2) {
            if(!nstr.empty()) {
                vec.push_back(std::make_tuple(nstr, std::string(), 0));
                nstr.clear();
            }
            vec.push_back(std::make_tuple(str, fmt, 1));
            i = n - 1;
        }
            
    }
    if(!nstr.empty()) {
        vec.push_back(std::make_tuple(nstr, std::string(), 0));
    }
    // [function] 引入function
    static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)>> s_format_items = {
        // {"m",[](const std::string& fmt) { return FormatItem::ptr(new MessageFormatItem(fmt)); } }
#define XX(str,C) \
        {#str, [] (const std::string& fmt) { return FormatItem::ptr(new C(fmt)); }}

        XX(m, MessageFormatItem),           //m:消息
        XX(p, LevelFormatItem),             //p:日志级别
        XX(r, EplaseFormatItem),            //r:累计毫秒数
        XX(c, LoggerNameFormatItem),              //c:日志名称
        XX(t, ThreadIdFormatItem),          //t:线程id
        XX(n, LineFormatItem),           //n:换行
        XX(d, DateTimeFormatItem),          //d:时间
        XX(l, LineFormatItem),              //l:行号
        XX(f, FiberIdFormatItem),           //F:协程id
#undef XX
    };
    for(auto& i : vec) {
        if(std::get<2>(i) == 0) {
            m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));
        } else {
            auto it = s_format_items.find(std::get<0>(i));
            if(it == s_format_items.end()) {
                m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));
            } else {
                m_items.push_back(it->second(std::get<1>(i)));
            }
        }
        std::cout << '(' << std::get<0>(i) << ") - (" << std::get<1>(i) << ") - (" << std::get<2>(i) << ')' << std::endl;
    }
}
}



8.3test.cc

#include <iostream>
#include "../sylar/log.h"

int main(int argc, char** argv) {
    sylar::Logger::ptr logger(new sylar::Logger);
    logger->addAppender(sylar::LogAppender::ptr(new sylar::StdoutAppender)); 

    sylar::LogEvent::ptr event(new sylar::LogEvent(__FILE__, __LINE__, 0, 1, 2, time(0)));
    // event->getSS() << "hello sylar log";
    logger->log(sylar::LogLevel::DEBUG, event);
    // std::cout << "hello sylar log" << std::endl;

    return 0;
}

8.4Cmakelists.txt

cmake_minimum_required(VERSION 2.8)
project(sylar)

set(CMAKE_VERBOSE_MAKEFILE ON) 
set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} -rdynamic -O0 -g -std=c++11 -Wall -Wno-deprecated -Werror -Wno-unused-function")

set(LIB_SRC
    sylar/log.cc
    # sylar/util.cc
    )

add_library(sylar SHARED ${LIB_SRC})
#add_library(sylar_static STATIC ${LIB_SRC})
#SET_TARGET_PROPERTIES(sylar_static PROPERTIES OUTPUT_NAME "sylar")

add_executable(test tests/test.cc)
add_dependencies(test sylar)
target_link_libraries(test sylar)

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

d::get<0>(i) << “) - (” << std::get<1>(i) << “) - (” << std::get<2>(i) << ‘)’ << std::endl;
}
}
}


### 8.3test.cc

```c
#include <iostream>
#include "../sylar/log.h"

int main(int argc, char** argv) {
    sylar::Logger::ptr logger(new sylar::Logger);
    logger->addAppender(sylar::LogAppender::ptr(new sylar::StdoutAppender)); 

    sylar::LogEvent::ptr event(new sylar::LogEvent(__FILE__, __LINE__, 0, 1, 2, time(0)));
    // event->getSS() << "hello sylar log";
    logger->log(sylar::LogLevel::DEBUG, event);
    // std::cout << "hello sylar log" << std::endl;

    return 0;
}

8.4Cmakelists.txt

cmake_minimum_required(VERSION 2.8)
project(sylar)

set(CMAKE_VERBOSE_MAKEFILE ON) 
set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} -rdynamic -O0 -g -std=c++11 -Wall -Wno-deprecated -Werror -Wno-unused-function")

set(LIB_SRC
    sylar/log.cc
    # sylar/util.cc
    )

add_library(sylar SHARED ${LIB_SRC})
#add_library(sylar_static STATIC ${LIB_SRC})
#SET_TARGET_PROPERTIES(sylar_static PROPERTIES OUTPUT_NAME "sylar")

add_executable(test tests/test.cc)
add_dependencies(test sylar)
target_link_libraries(test sylar)

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

madkeyboard

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值