github

github.com/idealvin/co

Reference documentation

  • 英 文: Github Gitee
  • English: github gitee

What is CO

CO is an elegant, efficient C++ base library for Linux, Windows, and Mac. It implements a golang-like coroutine, coroutine-based network programming framework, command line parameters and configuration file parsing library, high-performance log library, unit test framework, JSON library and a series of high-quality basic components.

CO is open source on Github under the MIT LICENSE. It uses part of the three-party code and may have different licenses. See the LICENSE file for details. Github code is also regularly synchronized on Gitee for the convenience of domestic users.

The development of CO

Alvin(idealvin) has been developing CO since 2013. The original purpose is to reduce the tripartite dependency in C++ projects and improve the development efficiency of C++. Since 2015, Alvin has introduced CO into actual projects for use by himself and his colleagues, greatly reducing the development cycle of projects and allowing CO to withstand the test of industrial projects.

After years of accumulation, Alvin implemented the coroutine mechanism in golang in C++ and provided a coroutine-based network programming framework in 2019. CO coroutine has been used in embedded network program development since its birth, and has achieved immediate results.

By 2021, CO coroutine has made great progress. At present, Hooks have been supported on Linux/Windows/Mac platforms, and coroutine lock, coroutine synchronism event, coroutine pool and Channel and Waitgroup in Golang have been implemented. Users can write golang’s experience in CO.

Quick learning

compile

You are advised to install xmake and run the following command in the CO root directory to build all subprojects:

xmake -a
Copy the code

If you need to use the HTTP ::Client, SSL, or HTTPS features, you can build with the following command:

xmake f --with_libcurl=true --with_openssl=true
xmake -a
Copy the code

Xmake automatically installs libcurl and OpenSSL from the network, which can be slow depending on the network. Xmake-a builds all the test code for libco, Gen, co/unitest, and co/test. The user can run the test program in CO by executing the following command:

xmake r unitest -a
xmake r flag
xmake r log -cout
xmake r co
Copy the code

Develop C++ projects using CO

In the simplest case, you can include co/all.h directly, using all the features in co. If you are worried about the compilation speed, you can include only the required header files, such as co/co.h, and use co/flag, co/log, and all the features associated with coroutines.

#include "co/all.h"

DEF_string(s, "nice"."");

int main(int argc, char** argv) {
    flag::init(argc, argv);
    log::init(a); LOG << FLG_s;return 0;
}
Copy the code

For a simple example, the first two lines of the main function are used to initialize the flag and log libraries, respectively. Some components in CO use flag to define configuration items and log to print logs. Therefore, you need to call flag::init() and log::init() at the beginning of the main function for initialization.

The user can also define the main function with the DEF_main macro:

#include "co/all.h"

DEF_string(s, "nice"."");

DEF_main(argc, argv) {
    LOG << FLG_s;
    return 0;
}
Copy the code

Flag ::init() and log::init() are already called internally by DEF_main. You do not need to call them again. In addition, DEF_main runs the code from the main function inside the coroutine, consistent with golang, whose main function is also inside the coroutine. Some coroutine-related components in CO must be used in coroutines. When developing coroutine-based applications with CO, it is generally recommended to define the main function with DEF_main.

Core components

co/flag

Co/Flag is an easy-to-use command line argument and configuration file parsing library that some components in CO use to define configuration items.

Co/Flag provides a default value for each configuration item. In the absence of configuration parameters, the program can run with the default configuration. You can also pass configuration parameters from the command line interface (CLI) or configuration file. If a configuration file is required, you can run./ exe-mkconf to automatically generate a configuration file.

// xx.cc
#include "co/flag.h"
#include "co/log.h"

DEF_bool(x, false."bool x");
DEF_bool(y, false."bool y");
DEF_uint32(u32, 0."...");
DEF_string(s, "hello world"."string");

int main(int argc, char** argv) {
    flag::init(argc, argv);

    COUT << "x: " << FLG_x;
    COUT << "y: " << FLG_y;
    COUT << "u32: " << FLG_u32;
    COUT << FLG_s << "|" << FLG_s.size(a);return 0;
}
Copy the code

Here is an example of using co/flag. The macro starting with DEF_ in the code defines four configuration items, each of which is equivalent to a global variable named FLG_ plus the configuration name. After compiling the above code, it can be run as follows:

./xx                  # Run with default configuration
./xx -xy -s good      # Single-letter bool flag can be set to true
./xx -s "I'm ok"      # String containing Spaces
./xx -u32 8k          # Integer can have units: k,m,g,t,p, case insensitive

./xx -mkconf          # Automatically generate configuration file xx.conf
./xx xx.conf          # Pass parameters from the configuration file
./xx -config xx.conf  With the #
Copy the code

co/log

Co/Log is a high-performance local logging system that is used by some components in CO to print logs.

Co /log Logs are classified into five levels: DEBUG, INFO, Warning, Error, and FATAL. Printing logs of the fatal level terminates program running. Users can print different levels of logs as follows:

DLOG << "hello " << 23;  // debug
LOG << "hello " << 23;   // info
WLOG << "hello " << 23;  // warning
ELOG << "hello " << 23;  // error
FLOG << "hello " << 23;  // fatal
Copy the code

Co /log also provides a set of CHECK macros that can be treated as enhanced assert, and they are also not cleared in debug mode.

void* p = malloc(32);
CHECK(p ! =NULL) < <"malloc failed..";
CHECK_NE(p, NULL) < <"malloc failed..";
Copy the code

When the CHECK assertion fails, co/log prints the function call stack information and terminates the program.

Co /log is very fast, and almost no memory allocation operations are required after the program runs steadily. Here are some test results, just for your reference:

  • co/log vs glog (single thread)

    platform google glog co/log
    win2012 HHD 1.6 MB/s 180MB/s
    win10 SSD 3.7 MB/s 560MB/s
    mac SSD 17MB/s 450MB/s
    linux SSD 54MB/s 1023MB/s
  • co/log vs spdlog (Windows)

    threads total logs co/log time(seconds) spdlog time(seconds)
    1 1000000 0.103619 0.482525
    2 1000000 0.202246 0.565262
    4 1000000 0.330694 0.722709
    8 1000000 0.386760 1.322471
  • co/log vs spdlog (Linux)

    threads total logs co/log time(seconds) spdlog time(seconds)
    1 1000000 0.096445 2.006087
    2 1000000 0.142160 3.276006
    4 1000000 0.181407 4.339714
    8 1000000 0.303968 4.700860

co/unitest

Co/Unitest is a simple and easy to use unit testing framework, many components in CO will use it to write unit test code, for the stability of CO to provide a guarantee.

#include "co/unitest.h"
#include "co/os.h"

namespace test {
    
DEF_test(os) {
    DEF_case(homedir) {
        EXPECT_NE(os::homedir(), "");
    }

    DEF_case(cpunum) {
        EXPECT_GT(os::cpunum(), 0); }}}// namespace test
Copy the code

For a simple example, the DEF_test macro defines a test unit, which is actually a function (a method in a class). The DEF_case macro defines test cases, each of which is essentially a block of code. Multiple test units can be placed in the same C++ project, and the main function usually needs only the following lines:

#include "co/unitest.h"

int main(int argc, char** argv) {
    flag::init(argc, argv);
    unitest::run_all_tests(a);return 0;
}
Copy the code

Under the co/unitest directory is the unit test code in co. After compilation, run the following command:

xmake r unitest -a   # Run all unit test cases
xmake r unitest -os  Run only test cases in the OS unit
Copy the code

coroutines

CO implements a golang-like coroutine with the following features:

  • Multithreaded scheduling. The default number of threads is the number of system CPU cores.
  • Shared stack, coroutines in the same thread share several stacks (default size is 1MB), memory usage is low, tests on Linux show that 10 million coroutines use only 2.8 GB of memory (for reference only).
  • Coroutines are horizontal, and new coroutines can be created anywhere, including within a coroutine.
  • Support system API hook (Windows/Linux/Mac), you can directly use the three-party network library in the coroutine.
  • Coprogramized socket API.
  • Coprogram simultaneous Event CO ::Event.
  • Coprogram lock CO ::Mutex.
  • Co-process Pool CO ::Pool.
  • The channel co: : Chan.
  • Waitgroup co: : waitgroup.

Create coroutines

go(ku);            // void ku();
go(f, 7);          // void f(int);
go(&T::f, &o);     // void T::f(); T o;
go(&T::f, &o, 7);  // void T::f(int); T o;
go([](){
    LOG << "hello go";
});
Copy the code

Go () is a function that takes one to three arguments. The first argument, f, is an arbitrary callable object. These parameters as long as meet the f (), (*) f (), f (p), (*) f (p), (o – > * f () or (- > * o f) (p) can be invoked.

The coroutines created by go() are evenly distributed among the different scheduling threads. If the user wants some coroutines to run in the same thread, the coroutines can be created as follows:

auto s = co::next_scheduler(a); s->go(f1);
s->go(f2);
Copy the code

If the user wants to create coroutines in all scheduled threads, he can do so:

auto& s = co::all_schedulers(a);for (size_t i = 0; i < s.size(a); ++i) { s[i]->go(f);
}
Copy the code

channel

Co ::Chan, similar to a channel in Golang, can be used to pass data between coroutines.

#include "co/co.h"

DEF_main(argc, argv) {
    co::Chan<int> ch;
    go([ch]() {
        ch << 7;
    });

    int v = 0;
    ch >> v;
    LOG << "v: " << v;

    return 0;
}
Copy the code

Channel reads and writes must be done inside the coroutine, so the code above defines the main function with DEF_main and makes the code inside main run in the coroutine.

The channel object in the code is on the stack, while CO adopts the shared stack implementation mode. The data on a coroutine stack may be covered by other coroutines. Generally, coroutines cannot directly communicate with each other through the data on the stack, so lambda in the code adopts the method of capturing by value and copies the channel. To the newly created coroutine. Copying a channel simply increases the internal reference count by one, with little performance impact.

When creating a channel, you can add a timeout as follows:

co::Chan<int> ch(8.1000);
Copy the code

Co ::timeout() can be called to determine whether the channel has timed out, which is simpler than the select-based implementation in Golang.

Channels in CO are implemented based on in-memory copy, and the data types passed can be built-in types, pointer types, or structure types with simple in-memory copy semantics for the copy operation. For container types like STD :: String or STL, copy operations are not simple memory copies and generally cannot be passed directly to a channel. See the co::Chan reference documentation for details.

waitgroup

Co ::WaitGroup, similar to Sync. WaitGroup in Golang, can be used to wait for the exit of a coroutine or thread.

#include "co/co.h"

DEF_main(argc, argv) {
    FLG_cout = true;

    co::WaitGroup wg;
    wg.add(8);

    for (int i = 0; i < 8; ++i) {
        go([wg]() {
            LOG << "co: " << co::coroutine_id(a); wg.done(a); }); } wg.wait(a);return 0;
}
Copy the code

Network programming

CO provides a set of coprogramized Socket API, most of them in form and the native socket API is basically the same, familiar with socket programming users, can easily write high-performance network programs in a synchronous way. In addition, CO implements higher-level network programming components, including TCP, HTTP, and jSON-based RPC frameworks that are IPv6 compatible and SSL enabled, and are more convenient to use than the Socket API. This is a brief demonstration of HTTP usage; for the rest, see the reference documentation.

Static web server

#include "co/flag.h"
#include "co/log.h"
#include "co/so.h"

DEF_string(d, "."."root dir"); // Specify the root directory of the web server

int main(int argc, char** argv) {
    flag::init(argc, argv);
    log::init(a); so::easy(FLG_d.c_str()); // mum never have to worry again

    return 0;
}
Copy the code

HTTP server

http::Server serv;

serv.on_req(
    [](const http::Req& req, http::Res& res) {
        if (req.is_method_get()) {
            if (req.url() = ="/hello") {
                res.set_status(200);
                res.set_body("hello world");
            } else {
                res.set_status(404); }}else {
            res.set_status(405); // method not allowed}}); serv.start("0.0.0.0".80);                                    // http
serv.start("0.0.0.0".443."privkey.pem"."certificate.pem"); // https
Copy the code

HTTP client

void f(a) {
    http::Client c("https://github.com");

    c.get("/");
    LOG << "response code: "<< c.response_code(a); LOG <<"body size: "<< c.body_size(a); LOG <<"Content-Length: "<< c.header("Content-Length");
    LOG << c.header(a); c.post("/hello"."data xxx");
    LOG << "response code: "<< c.response_code(a); }go(f);
Copy the code