顯示具有 函式庫 標籤的文章。 顯示所有文章
顯示具有 函式庫 標籤的文章。 顯示所有文章

2026/04/14

C++ Crow

C++ 並不是網頁開發(Web Development)的首選語言, 但是在對效能十分要求或者是需要在資源略為受限的環境執行時,可以考慮使用 C++。 就目前而言,C++ 較為有名的選擇為效能極高並且支援多項功能的 Drogon, 不用安裝依賴函式庫的 Oat++, 以及設計上較為輕量但是功能也較少的 Crow。 還有一個選擇是 Wt, 但是就我個人而言,我認為 Wt 的策略,也就是使用 C++ 撰寫 UI 然後函式庫再轉譯為 HTML/CSS/JavaScript 的方式並不是一個好的做法。

Crow 是一套 C++ micro web framework,採用 Header Only 的設計,其靈感來自於 Python's Flask, 支援 HTTP 1.1 以及 Websocket,使用 C++ ASIO library 構建,特別適合建立 RESTful API 或 Web 服務。 Crow 的原作者在 2017 年停止維護, 不過之後自由軟體社群有人複製出來一份新的分支接手進行維護,目前仍然持續開發中。

在安裝前需要已經先安裝 ASIO development files,下面是在 openSUSE 的安裝指令:

sudo zypper in asio-devel

我使用 source code 安裝:

git clone https://github.com/CrowCpp/Crow.git
mkdir build; cd build; cmake .. -DCROW_BUILD_EXAMPLES=OFF -DCROW_BUILD_TESTS=OFF
sudo make install

(在不編譯範例以及測試程式的情況下,Crow 只會安裝 CMake 相關檔案以及 header files,所以不用執行 make, 只需要使用 make install 安裝)

Crow 預設的靜態檔案資源放置在 static 目錄,但是可以透過巨集設定目錄。接下來寫一個簡單的靜態網頁伺服器驗證可以正確編譯與使用。

main.cpp

#define CROW_STATIC_DIRECTORY "public"
#define CROW_STATIC_ENDPOINT "/<path>"
#include "crow.h"

int main()
{
    crow::SimpleApp app;

    CROW_ROUTE(app, "/")
    ([](const crow::request&, crow::response& res) {
        res.set_static_file_info("public/index.html");
        res.end();
    });

    app.port(18080).run();
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(Simple)

set(CMAKE_CXX_STANDARD 17)

# Search for Crow and required dependencies
find_package(Crow REQUIRED)

add_executable(simple main.cpp)

# Link the Crow library to your executable
target_link_libraries(simple PRIVATE Crow::Crow)

Crow 使用 Mustache 作為 template engine language, 接下建立一個簡單的網頁進行測試。 使用 Mustache 撰寫的網頁需要放在 templates 目錄下。

templates/fancypage.html

<!DOCTYPE html>
<html>
  <body>
  <p>Hello {{person}}!</p>
</body>
</html>

main.cpp

#include "crow.h"
// #include "crow_all.h"

int main() {
    crow::SimpleApp app;

    CROW_ROUTE(app, "/<string>")([](std::string name) {
        auto page = crow::mustache::load("fancypage.html");
        crow::mustache::context ctx({{"person", name}});
        return page.render(ctx);
    });

    app.port(18080).multithreaded().run();
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(Simple)

set(CMAKE_CXX_STANDARD 17)

# Search for Crow and required dependencies
find_package(Crow REQUIRED)

add_executable(simple main.cpp)

# Link the Crow library to your executable
target_link_libraries(simple PRIVATE Crow::Crow)

編譯成功後執行,使用瀏覽器瀏覽 http://localhost:18080/Bob 進行測試。

參考連結

2025/11/29

Boost.JSON

Boost.JSON 是一個 C++ JSON parser 函式庫, 提供了雖然不是最快但是也足夠快的執行效率、以及雖然不是最方便但是足以滿足使用者需要的便利使用方式, 就綜合條件來說,我認為是十分優秀的 C++ JSON parser 函式庫。 他有二個使用方式,第一種需要連結函式庫:

#include <boost/json.hpp>

第二種是 header-only:

#include <boost/json/src.hpp>

下面是從一個字串分析 JSON 的測試:

#include <boost/json.hpp>
#include <iostream>
#include <string>

namespace json = boost::json;

int main() {
    const std::string json_str = R"(
        {
            "user": "johndoe",
            "id": 12345,
            "active": true,
            "numbers": [1, 2, 3, 4, 5]
        }
    )";

    // Parse the JSON string
    json::value data = json::parse(json_str);

    // Access the values
    std::string username = json::value_to<std::string>(data.at("user"));
    int user_id = json::value_to<int>(data.at("id"));
    bool is_active = json::value_to<bool>(data.at("active"));

    std::cout << "Username: " << username << std::endl;
    std::cout << "ID: " << user_id << std::endl;
    std::cout << "Active: " << (is_active ? "Yes" : "No") << std::endl;

    // For array
    json::array &arr = data.at("numbers").as_array();
    std::vector<int> numbers;
    for (auto const &value : arr) {
        numbers.push_back(json::value_to<int>(value));
    }

    std::cout << "Parsed Numbers: ";
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

使用 CMake 編譯,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(parse)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS json)

add_executable(parse parse.cpp)
target_link_libraries(parse PRIVATE Boost::json)

如果採用 header-only 的方式,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(parse)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS)

add_executable(parse parse.cpp)
target_link_libraries(parse)

下面是建立 JSON 內容的測試:

#include <boost/json.hpp>
#include <iostream>
#include <string>

namespace json = boost::json;

int main() {
    // Create a JSON object
    json::object obj;
    obj["user"] = "johndoe";
    obj["id"] = 12345;
    obj["active"] = true;

    // Create a JSON array
    json::array numbers;
    numbers.push_back(1);
    numbers.push_back(2);
    numbers.push_back(3);
    numbers.push_back(4);
    numbers.push_back(5);

    obj["numbers"] = numbers;

    // Serialize the object to a string
    std::string serialized_json = json::serialize(obj);

    std::cout << "Generated JSON: " << serialized_json << std::endl;

    return 0;
}

參考連結

Asio C++ Library

Asio C++ Library 是一個免費、開放原始碼、跨平台的 C++ 網路程式庫。 它為開發者提供一致的非同步 I/O 模型(包含 Timer、File、Pipe、Serial Port 以及網路協定 TCP, UDP 與 ICMP), Boost.Asio 在 20 天的審查後,於 2005 年 12 月 30 日被 Boost 函式庫接納。 目前 Asio C++ Library 提供二種函式庫,一種可以獨立使用的 Asio C++ library,一種是與 Boost 函式庫整合的 Boost.Asio, 二種函式庫的核心相同,差別在於 Boost.Asio 跟隨 Boost 函式庫的發佈時程(這表示當 bugs 修正的時候, 有時候會慢一點才會隨著 Boost 的新版更正)。因為已經有安裝 Boost 函式庫,所以我使用的是 Boost.Asio。

Asio 在設計上使用 Proactor pattern。 Proactor 是一種用於事件處理的軟體設計模式,其中耗時較長的活動在非同步部分運行(在 Asio 就是 I/O 處理的部份)。 非同步部分終止後,會呼叫完成處理程序。 所有使用 asio 的程式都需要至少一個 I/O execution context,例如 io_context 或 thread_pool 物件。 I/O execution context 提供對 I/O 功能的存取。如果是非同步的操作,那麼需要實作 completion handler 來提供工作完成之後的通知目標。

下面是一個測試的程式,來自 Asio 教學網頁的 Using a timer synchronously。 boost::asio::io_context 就是執行 I/O 的部份。

#include <boost/asio.hpp>
#include <iostream>

int main() {
    boost::asio::io_context io;

    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(3));
    t.wait();

    std::cout << "Hello, world!" << std::endl;

    return 0;
}

使用 CMake 編譯,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(timer)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS)

add_executable(timer timer.cpp)

Using a timer asynchronously

使用 asio 的非同步功能意味著需要一個 completion token,該 token 決定了非同步操作完成後如何將結果傳遞給完成處理程序。 在這裡使用 print 函數,該函數將在非同步等待結束後被呼叫。

務必記住,在呼叫 boost::asio::io_context::run() 之前,要先給 io_context 一些工作。 如果沒指定一些工作(在本例中是 steady_timer::async_wait()),boost::asio::io_context::run() 會立即返回。

#include <boost/asio.hpp>
#include <iostream>

void print(const boost::system::error_code & /*e*/) {
    std::cout << "Hello, world!" << std::endl;
}

int main() {
    boost::asio::io_context io;

    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(3));
    t.async_wait(&print);

    io.run();

    return 0;
}

Binding arguments to a completion handler

要使用 asio 實作重複定時器,需要在完成處理程序中更改定時器的過期時間,然後啟動新的非同步等待。 這意味著 completion handler 需要能夠存取定時器物件。

#include <boost/asio.hpp>
#include <functional>
#include <iostream>

void print(const boost::system::error_code & /*e*/,
           boost::asio::steady_timer *t, int *count) {
    if (*count < 5) {
        std::cout << *count << std::endl;
        ++(*count);

        t->expires_at(t->expiry() + boost::asio::chrono::seconds(1));
        t->async_wait(
            std::bind(print, boost::asio::placeholders::error, t, count));
    }
}

int main() {
    boost::asio::io_context io;

    int count = 0;
    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(1));
    t.async_wait(
        std::bind(print, boost::asio::placeholders::error, &t, &count));

    io.run();

    std::cout << "Final count is " << count << std::endl;

    return 0;
}

Using a member function as a completion handler

std::bind 函式對類別成員函式和函式同樣有效。由於所有非靜態類別成員函數都有一個隱式的 this 參數,我們需要將 this 綁定到函數上。 std::bind 將我們的 completion handler(現在是成員函數)轉換為函數對象。

#include <boost/asio.hpp>
#include <functional>
#include <iostream>

class printer {
public:
    printer(boost::asio::io_context &io)
        : timer_(io, boost::asio::chrono::seconds(1)), count_(0) {
        timer_.async_wait(std::bind(&printer::print, this));
    }

    ~printer() { std::cout << "Final count is " << count_ << std::endl; }

    void print() {
        if (count_ < 5) {
            std::cout << count_ << std::endl;
            ++count_;

            timer_.expires_at(timer_.expiry() +
                              boost::asio::chrono::seconds(1));
            timer_.async_wait(std::bind(&printer::print, this));
        }
    }

private:
    boost::asio::steady_timer timer_;
    int count_;
};

int main() {
    boost::asio::io_context io;
    printer p(io);
    io.run();

    return 0;
}

Synchronising completion handlers in multithreaded programs

strand class template 是 executor adapter,它保證透過它分發的處理程序,在下一個處理程序啟動之前, 目前正在執行的處理程序必須完成。無論呼叫 boost::asio::io_context::run() 的執行緒數是多少,此保證都有效。 當然,這些處理程序仍然可能與其他未透過 strand 分發的處理程序,或透過不同 strand 物件分發的處理程序並發執行。


#include <boost/asio.hpp>
#include <functional>
#include <iostream>
#include <thread>

class printer {
public:
    printer(boost::asio::io_context &io)
        : strand_(boost::asio::make_strand(io)),
          timer1_(io, boost::asio::chrono::seconds(1)),
          timer2_(io, boost::asio::chrono::seconds(1)), count_(0) {
        timer1_.async_wait(boost::asio::bind_executor(
            strand_, std::bind(&printer::print1, this)));

        timer2_.async_wait(boost::asio::bind_executor(
            strand_, std::bind(&printer::print2, this)));
    }

    ~printer() { std::cout << "Final count is " << count_ << std::endl; }

    void print1() {
        if (count_ < 10) {
            std::cout << "Timer 1: " << count_ << std::endl;
            ++count_;

            timer1_.expires_at(timer1_.expiry() +
                               boost::asio::chrono::seconds(1));

            timer1_.async_wait(boost::asio::bind_executor(
                strand_, std::bind(&printer::print1, this)));
        }
    }

    void print2() {
        if (count_ < 10) {
            std::cout << "Timer 2: " << count_ << std::endl;
            ++count_;

            timer2_.expires_at(timer2_.expiry() +
                               boost::asio::chrono::seconds(1));

            timer2_.async_wait(boost::asio::bind_executor(
                strand_, std::bind(&printer::print2, this)));
        }
    }

private:
    boost::asio::strand<boost::asio::io_context::executor_type> strand_;
    boost::asio::steady_timer timer1_;
    boost::asio::steady_timer timer2_;
    int count_;
};

int main() {
    boost::asio::io_context io;
    printer p(io);
    std::thread t([&] { io.run(); });
    io.run();
    t.join();

    return 0;
}

File

Linux io_uring 在 Kernel 5.1 加入,其主要目標是透過高效率的非同步 I/O 框架,解決傳統 I/O 模型中系統呼叫和上下文切換的效能瓶頸, 移除傳統同步I/O 與 epoll 就緒通知模型需要頻繁切換使用者空間與核心空間的負擔,進而大幅提升系統在處理大量並發 I/O 操作時的效能。 liburing 是 Jens Axboe 維護的輔助函式庫,其主要目的是簡化 io_uring 的使用。 Asio 對於 Linux liburing 提供了包裝(目前需要使用者使用 flag 啟用),下面是我測試的程式, 讀取 /etc/os-release 取得 Linux Distribution Name:

#include <boost/asio.hpp>
#include <boost/asio/stream_file.hpp>
#include <filesystem>
#include <iostream>
#include <vector>

namespace asio = boost::asio;
namespace fs = std::filesystem;

std::vector<std::string> split(const std::string &str,
                               const std::string &delim) {
    std::vector<std::string> tokens;
    size_t prev = 0, pos = 0;
    do {
        pos = str.find(delim, prev);
        if (pos == std::string::npos)
            pos = str.length();
        std::string token = str.substr(prev, pos - prev);
        if (!token.empty())
            tokens.push_back(token);
        prev = pos + delim.length();
    } while (pos < str.length() && prev < str.length());

    return tokens;
}

void read_next_line(asio::stream_file &file, asio::streambuf &buffer) {
    asio::async_read_until(file, buffer, '\n',
                           [&](const boost::system::error_code &ec,
                               std::size_t bytes_transferred) {
                               if (!ec) {
                                   std::istream is(&buffer);
                                   std::string line;
                                   std::getline(is, line);

                                   auto splitArray = split(line, "=");
                                   if (splitArray[0].compare("NAME") == 0) {
                                       std::cout << splitArray[1] << std::endl;
                                   } else {
                                       read_next_line(file, buffer);
                                   }
                               } else if (ec == asio::error::eof) {
                                   std::cout << "End of file reached."
                                             << std::endl;
                               } else {
                                   std::cerr
                                       << "Error reading file: " << ec.message()
                                       << std::endl;
                               }
                           });
}

int main() {
    fs::path test_file_path = "/etc/os-release";

    asio::io_context io_context;

    boost::system::error_code ec_open;
    asio::stream_file file(io_context);
    file.open(test_file_path.string(), asio::stream_file::read_only, ec_open);

    if (ec_open) {
        std::cerr << "Failed to open file: " << ec_open.message() << std::endl;
        return 1;
    }

    asio::streambuf buffer;
    read_next_line(file, buffer);

    io_context.run();
    file.close();

    return 0;
}

使用 CMake 編譯,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(name)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(PkgConfig REQUIRED)
pkg_check_modules(uring REQUIRED IMPORTED_TARGET liburing)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS)

add_executable(name name.cpp)
target_link_libraries(name PRIVATE PkgConfig::uring)
target_compile_definitions(name PRIVATE BOOST_ASIO_HAS_IO_URING BOOST_ASIO_DISABLE_EPOLL)

Tcp

A synchronous TCP daytime client

我們需要將作為參數傳遞給應用程式的伺服器名稱轉換為 TCP 端點。為此,我們使用 ip::tcp::resolver 物件。 resolver 接收主機名稱和服務名,並將它們轉換為端點列表。 程式接下來建立並連接 Socket。上面獲得的端點列表可能同時包含 IPv4 和 IPv6 端點,因此我們需要逐一嘗試,直到找到可用的端點。 這樣可以確保客戶端程式與特定的 IP 版本無關。boost::asio::connect() 函數會自動執行此操作。

#include <array>
#include <boost/asio.hpp>
#include <iostream>

namespace asio = boost::asio;

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        asio::ip::tcp::resolver resolver(io_context);
        asio::ip::tcp::resolver::results_type endpoints =
           resolver.resolve(argv[1], "daytime");

        asio::ip::tcp::socket socket(io_context);
        asio::connect(socket, endpoints);

        for (;;) {
            std::array<char, 128> buf;
            boost::system::error_code error;

            size_t len = socket.read_some(asio::buffer(buf), error);

            if (error == asio::error::eof)
                break; // Connection closed cleanly by peer.
            else if (error)
                throw boost::system::system_error(error); // Some other error.

            std::cout.write(buf.data(), len);
        }
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

A synchronous TCP daytime server

需要建立一個 ip::tcp::acceptor 物件來監聽新連線。它被初始化為監聽 TCP 連接埠 13,支援 IP 版本 6。

#include <boost/asio.hpp>
#include <ctime>
#include <iostream>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

int main() {
    try {
        asio::io_context io_context;

        asio::ip::tcp::acceptor acceptor(
            io_context, asio::ip::tcp::endpoint(asio::ip::tcp::v6(), 13));

        for (;;) {
            asio::ip::tcp::socket socket(io_context);
            acceptor.accept(socket);

            std::string message = make_daytime_string();

            boost::system::error_code ignored_error;
            asio::write(socket, boost::asio::buffer(message), ignored_error);
        }
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

An asynchronous TCP daytime server

#include <boost/asio.hpp>
#include <ctime>
#include <functional>
#include <iostream>
#include <memory>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

class tcp_connection : public std::enable_shared_from_this<tcp_connection> {
public:
    typedef std::shared_ptr<tcp_connection> pointer;

    static pointer create(asio::io_context &io_context) {
        return pointer(new tcp_connection(io_context));
    }

    asio::ip::tcp::socket &socket() { return socket_; }

    void start() {
        message_ = make_daytime_string();

        asio::async_write(socket_, asio::buffer(message_),
                          std::bind(&tcp_connection::handle_write,
                                    shared_from_this(),
                                    asio::placeholders::error,
                                    asio::placeholders::bytes_transferred));
    }

private:
    tcp_connection(asio::io_context &io_context) : socket_(io_context) {}

    void handle_write(const boost::system::error_code & /*error*/,
                      size_t /*bytes_transferred*/) {}

    asio::ip::tcp::socket socket_;
    std::string message_;
};

class tcp_server {
public:
    tcp_server(asio::io_context &io_context)
        : io_context_(io_context),
          acceptor_(io_context,
                    asio::ip::tcp::endpoint(asio::ip::tcp::v6(), 13)) {
        start_accept();
    }

private:
    void start_accept() {
        tcp_connection::pointer new_connection =
            tcp_connection::create(io_context_);

        acceptor_.async_accept(new_connection->socket(),
                               std::bind(&tcp_server::handle_accept, this,
                                         new_connection,
                                         asio::placeholders::error));
    }

    void handle_accept(tcp_connection::pointer new_connection,
                       const boost::system::error_code &error) {
        if (!error) {
            new_connection->start();
        }

        start_accept();
    }

    asio::io_context &io_context_;
    asio::ip::tcp::acceptor acceptor_;
};

int main() {
    try {
        asio::io_context io_context;
        tcp_server server(io_context);
        io_context.run();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

下面是我的練習程式,將 client 改寫為 asynchronous:

#include <boost/asio.hpp>
#include <iostream>
#include <vector>

namespace asio = boost::asio;

const int BUFFER_SIZE = 128;

void handle_read(const boost::system::error_code &error,
                 std::size_t bytes_transferred, asio::ip::tcp::socket &socket,
                 std::vector<char> &buffer) {
    if (!error) {
        for (std::size_t i = 0; i < bytes_transferred; ++i) {
            std::cout << buffer[i];
        }
    } else {
        std::cerr << "Error during read: " << error.message() << std::endl;
    }
}

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        asio::ip::tcp::resolver resolver(io_context);
        asio::ip::tcp::resolver::results_type endpoints =
            resolver.resolve(argv[1], "daytime");

        asio::ip::tcp::socket socket(io_context);
        asio::connect(socket, endpoints);

        std::vector<char> buffer(BUFFER_SIZE);
        socket.async_read_some(asio::buffer(buffer),
                               std::bind(handle_read, std::placeholders::_1,
                                         std::placeholders::_2,
                                         std::ref(socket), std::ref(buffer)));

        io_context.run();
        socket.close();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

UDP

A synchronous UDP daytime client

我們使用 ip::udp::resolver 物件,根據主機名稱和服務名稱尋找要使用的正確遠端端點。 透過 ip::udp::v6() 參數,查詢被限制為僅傳回 IPv6 端點。 如果 ip::udp::resolver::resolve()函數沒有失敗,則保證至少會傳回清單中的一個端點。這意味著直接解引用回傳值是安全的。

#include <boost/asio.hpp>
#include <array>
#include <iostream>

namespace asio = boost::asio;

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        asio::ip::udp::resolver resolver(io_context);
        asio::ip::udp::endpoint receiver_endpoint =
            *resolver.resolve(asio::ip::udp::v6(), argv[1], "daytime").begin();

        asio::ip::udp::socket socket(io_context);
        socket.open(asio::ip::udp::v6());

        std::array<char, 1> send_buf = {{0}};
        socket.send_to(asio::buffer(send_buf), receiver_endpoint);

        std::array<char, 128> recv_buf;
        asio::ip::udp::endpoint sender_endpoint;
        size_t len =
            socket.receive_from(asio::buffer(recv_buf), sender_endpoint);

        std::cout.write(recv_buf.data(), len);
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

A synchronous UDP daytime server

#include <boost/asio.hpp>
#include <array>
#include <ctime>
#include <iostream>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

int main() {
    try {
        asio::io_context io_context;

        asio::ip::udp::socket socket(
            io_context, asio::ip::udp::endpoint(asio::ip::udp::v6(), 13));

        for (;;) {
            std::array<char, 1> recv_buf;
            asio::ip::udp::endpoint remote_endpoint;
            socket.receive_from(asio::buffer(recv_buf), remote_endpoint);

            std::string message = make_daytime_string();

            boost::system::error_code ignored_error;
            socket.send_to(asio::buffer(message), remote_endpoint, 0,
                           ignored_error);
        }
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

An asynchronous UDP daytime server

#include <boost/asio.hpp>
#include <array>
#include <ctime>
#include <functional>
#include <iostream>
#include <memory>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

class udp_server {
public:
    udp_server(asio::io_context &io_context)
        : socket_(io_context,
                  asio::ip::udp::endpoint(asio::ip::udp::v6(), 13)) {
        start_receive();
    }

private:
    void start_receive() {
        socket_.async_receive_from(
            asio::buffer(recv_buffer_), remote_endpoint_,
            std::bind(&udp_server::handle_receive, this,
                      asio::placeholders::error,
                      asio::placeholders::bytes_transferred));
    }

    void handle_receive(const boost::system::error_code &error,
                        std::size_t /*bytes_transferred*/) {
        if (!error) {
            std::shared_ptr<std::string> message(
                new std::string(make_daytime_string()));

            socket_.async_send_to(
                asio::buffer(*message), remote_endpoint_,
                std::bind(&udp_server::handle_send, this, message,
                          asio::placeholders::error,
                          asio::placeholders::bytes_transferred));

            start_receive();
        }
    }

    void handle_send(std::shared_ptr<std::string> /*message*/,
                     const boost::system::error_code & /*error*/,
                     std::size_t /*bytes_transferred*/) {}

    asio::ip::udp::socket socket_;
    asio::ip::udp::endpoint remote_endpoint_;
    std::array<char, 1> recv_buffer_;
};

int main() {
    try {
        asio::io_context io_context;
        udp_server server(io_context);
        io_context.run();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

下面是我的練習程式,將 client 改寫為 asynchronous:

#include <array>
#include <boost/asio.hpp>
#include <iostream>

namespace asio = boost::asio;

class udp_client {
public:
    udp_client(asio::io_context &io_context, const std::string &host)
        : socket_(io_context) {
        asio::ip::udp::resolver resolver(io_context);
        remote_endpoint_ =
            *resolver.resolve(asio::ip::udp::v6(), host, "daytime").begin();

        socket_.open(asio::ip::udp::v6());

        start_send();
    }

private:
    void start_send() {
        socket_.async_send_to(asio::buffer(send_buffer_), remote_endpoint_,
                              std::bind(&udp_client::handle_send, this,
                                        asio::placeholders::error,
                                        asio::placeholders::bytes_transferred));
    }

    void handle_send(const boost::system::error_code &error,
                     std::size_t /*bytes_transferred*/) {
        if (!error) {
            socket_.async_receive_from(
                asio::buffer(recv_buffer_), remote_endpoint_,
                std::bind(&udp_client::handle_receive, this,
                          asio::placeholders::error,
                          asio::placeholders::bytes_transferred));
        } else {
            std::cerr << "Error during send: " << error.message() << std::endl;
        }
    }

    void handle_receive(const boost::system::error_code &error,
                        std::size_t bytes_transferred) {

        if (!error) {
            std::cout.write(recv_buffer_.data(), bytes_transferred);
        } else {
            std::cerr << "Error during receive: " << error.message()
                      << std::endl;
        }
    }

    asio::ip::udp::socket socket_;
    asio::ip::udp::endpoint remote_endpoint_;
    std::array<char, 1> send_buffer_ = {{0}};
    std::array<char, 128> recv_buffer_;
};

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        udp_client client(io_context, argv[1]);

        io_context.run();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

相關連結

2025/11/07

SOCI

SOCI (Simple Oracle Call Interface) 一開始是由 Maciej Sobczak 在 CERN 工作時開發, 作為 Oracle 資料庫函式庫的 abstraction layer 並且在 CERN 的工作環境中使用,之後則又加入了數個資料庫的支援。

SOCI 目前支援 Oracle, MySQL, PostgreSQL, SQLite3 等資料庫以及 ODBC 作為通用的 backend。 下面是在 openSUSE 安裝 SOCI core 以及 SOCI SQLite3 開發檔案的指令:
sudo zypper in soci-devel soci-sqlite3-devel

(注意:SOCI 本身的體積並不大,但是安裝時需要 boost-devel,而 boost-devel 是個包含很多模組的函式庫, 如果之前就有安裝 boost-devel 那麼這就不是一個問題)

下面是一個使用 SOCI 取得 SQLite3 版本的測試程式:

#include <soci/soci.h>
#include <soci/sqlite3/soci-sqlite3.h>

#include <iostream>
#include <string>

int main() {
    try {
        soci::session sql("sqlite3", "db=:memory:");

        std::string version;
        sql << "select sqlite_version()", soci::into(version);
        std::cout << version << std::endl;
    } catch (soci::soci_error const &e) {
        std::cerr << "Failed: " << e.what() << std::endl;
    } catch (std::runtime_error const &e) {
        std::cerr << "Unexpected standard exception occurred: " << e.what()
                  << std::endl;
    } catch (...) {
        std::cerr << "Unexpected unknown exception occurred." << std::endl;
    }

    return 0;
}

使用下列的指令編譯:

g++ version.cpp -lsoci_core -lsoci_sqlite3 -o version

下面是在 openSUSE 安裝 SOCI core 以及 SOCI ODBC 開發檔案的指令:

sudo zypper in soci-devel soci-odbc-devel

下面是一個使用 SOCI ODBC 取得 PostgreSQL 版本的測試程式:

#include <soci/odbc/soci-odbc.h>
#include <soci/soci.h>

#include <iostream>
#include <string>

int main() {
    try {
        soci::session sql("odbc",
                          "DSN=PostgreSQL; UID=postgres; PWD=postgres;");

        std::string version;
        soci::rowset<std::string> rs = (sql.prepare << "select version()");

        for (soci::rowset<std::string>::const_iterator it = rs.begin();
             it != rs.end(); ++it) {
            std::cout << *it << std::endl;
        }
    } catch (soci::soci_error const &e) {
        std::cerr << "Failed: " << e.what() << std::endl;
    } catch (std::runtime_error const &e) {
        std::cerr << "Unexpected standard exception occurred: " << e.what()
                  << std::endl;
    } catch (...) {
        std::cerr << "Unexpected unknown exception occurred." << std::endl;
    }

    return 0;
}

使用下列的指令編譯:

g++ version.cpp -lsoci_core -lsoci_odbc -o version

參考連結

2025/10/19

Fast Light Toolkit

FLTK 由 Bill Spitzak 個人開始開發並且目前仍然是專案的領導者, 是一個跨平台的 C++ GUI toolkit,用在 UNIX/Linux、微軟 Windows 和 Mac OS X 上, 使用 OpenGL 相關的功能提供 3D 繪圖的支援,並且有一個簡單的界面設計工具 FLUID。 FLTK 被設計足夠小和模組化以被靜態連結,但作為共享庫也工作良好。FLTK 的授權使用 LGPL v2,不過允許使用靜態連結。

一般而言,跨平台 GUI toolkit 有以下二個主要的策略:

  • 使用一組共同的 API 包裝各平台提供的底層函式庫或者是工具箱,如果該平台沒有提供相關的視窗元件 (widget) 再自行撰寫。 使用此策略著名的 GUI toolkit 為 wxWidgets。
  • 使用各個平台的繪圖功能繪製出最基本的視窗,再由最基本的視窗建構各種視窗元件。 使用此策略著名的 GUI toolkit 為 GTK 與 Qt。

FLTK 的跨平台策略為使用各個平台的繪圖功能繪製出最基本的視窗,再由最基本的視窗建構各種視窗元件。 這個策略的優點是移植到不同的平台較為容易,缺點是缺少原生的外觀和感覺 (native look and feel), 需要使用佈景主題 (theme) 之類的方法改進視覺效果。

FLTK 是一個輕量化的函式庫,適合撰寫一些小工具或者是提供前端界面,不過雖然提供了大多數常見的視窗元件, 但是如果是很複雜的視窗元件則未必有提供(有可能需要自行撰寫),如果要撰寫極為複雜的圖形使用者介面應用程式,那麼建議使用其它的 GUI toolkit。

從 FLTK 1.4 開始,編譯 FLTK 建議使用的 build system 為 CMake。


所有 FLTK 應用程式都基於事件處理模型。使用者滑鼠移動、按鈕點擊和鍵盤活動等操作都會產生事件, 並發送給應用程式。然後,應用程式可能會忽略事件或回應用戶。

下面就是 FLTK 的 Hello 例子:

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>

int main(int argc, char **argv) {
    Fl_Window *window = new Fl_Window(340, 180);

    Fl_Box *box = new Fl_Box(20, 40, 300, 100, "Hello, World!");
    box->box(FL_UP_BOX);
    box->labelfont(FL_BOLD + FL_ITALIC);
    box->labelsize(36);
    box->labeltype(FL_SHADOW_LABEL);

    window->end();
    window->show(argc, argv);
    return Fl::run();
}

在 FLTK 1.3.x,需要宣告 <FL/Fl.H> 才行,FLTK 1.4 則放鬆了這個限制,不過宣告了也不會有問題。

FLTK 提供了 fltk-config 可以用來查詢編譯與連結時需要的參數,下面是在 UNIX/Linux 環境下編譯的指令:

g++ hello.cxx -o hello `fltk-config --cxxflags --ldflags`

如果要使用 CMake 作為 build system,那麼建立一個 CMakeLists.txt,內容如下:

cmake_minimum_required(VERSION 3.15)

project(hello)

set(FLTK_DIR "/usr/local"
CACHE FILEPATH "FLTK installation or build directory")

find_package(FLTK 1.4 CONFIG REQUIRED)

MESSAGE ( STATUS "  FLTK_FOUND :          " ${FLTK_FOUND} )

add_executable       (hello WIN32 MACOSX_BUNDLE hello.cxx)
target_link_libraries(hello PRIVATE fltk::fltk)

注意:如果自行編譯 FLTK,其預設安裝位置為 /usr/local,但是使用者可以設定其安裝位置。 下面是安裝到自己家目錄下子目錄的例子。

cmake .. -DCMAKE_INSTALL_PREFIX=/home/danilo/Programs/fltk

如果改變預設安裝位置,結果 CMake 無法找到 FLTK,可以嘗試設定 CMAKE_PREFIX_PATH。

cmake .. -DCMAKE_PREFIX_PATH=/home/danilo/Programs/fltk

也可以在 CMakeLists.txt 設定:

cmake_minimum_required(VERSION 3.15)

project(hello)

set(CMAKE_PREFIX_PATH "/home/danilo/Programs/fltk")
set(FLTK_DIR "/home/danilo/Programs/fltk"
CACHE FILEPATH "FLTK installation or build directory")

find_package(FLTK 1.4 CONFIG REQUIRED)

MESSAGE ( STATUS "  FLTK_FOUND :          " ${FLTK_FOUND} )

add_executable       (hello WIN32 MACOSX_BUNDLE hello.cxx)
target_link_libraries(hello PRIVATE fltk::fltk)

下面更新的 Hello 例子,加入了設定整體的前景顏色與背景顏色,並且使用 Fl::scheme 設定視窗元件的顯示方案。

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Window.H>

int main(int argc, char **argv) {
    uchar r, g, b;
    Fl::get_color(FL_BLACK, r, g, b);
    Fl::foreground(r, g, b);
    Fl::get_color(FL_WHITE, r, g, b);
    Fl::background(r, g, b);
    Fl::get_color(FL_WHITE, r, g, b);
    Fl::background2(r, g, b);

    Fl::scheme("gtk+");

    Fl_Window *window = new Fl_Window(640, 480);

    Fl_Box *box = new Fl_Box(20, 40, 450, 100, "Hello, World!");
    box->box(FL_UP_BOX);
    box->color(FL_BLACK);
    box->labelfont(FL_BOLD + FL_ITALIC);
    box->labelsize(36);
    box->labeltype(FL_SHADOW_LABEL);
    box->labelcolor(FL_RED);

    Fl_Box *box2 = new Fl_Box(20, 180, 450, 100, "Fast Light Toolkit");
    box2->box(FL_NO_BOX);
    box2->labelsize(36);
    box2->labeltype(FL_SHADOW_LABEL);

    window->end();
    window->show(argc, argv);
    return Fl::run();
}

FLTK 提供了一些顯示文字的視窗元件,下面是使用 Fl_Text_Display 的例子。

// Fl_Text_Display example. -erco
#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Text_Display.H>

Fl_Double_Window *app_win = nullptr;
Fl_Text_Buffer *buff = nullptr;
Fl_Text_Display *disp = nullptr;

void my_build_app_window() {
    app_win = new Fl_Double_Window (640, 480, "Display");

    buff = new Fl_Text_Buffer();
    disp = new Fl_Text_Display(20, 20, 640 - 40, 480 - 40, "Display Text");
    disp->buffer(buff);
    app_win->resizable(*disp);

    buff->text("line 0\nline 1\nline 2\n"
               "line 3\nline 4\nline 5\n"
               "line 6\nline 7\nline 8\n"
               "line 9\nline 10\nline 11\n"
               "line 12\nline 13\nline 14\n"
               "line 15\nline 16\nline 17\n"
               "line 18\nline 19\nline 20\n"
               "line 21\nline 22\nline 23\n");
}

int main (int argc, char **argv) {
    my_build_app_window();
    app_win->show(argc, argv);
    return (Fl::run());
}

Callbacks

Callbacks 是當視窗元件的值改變時所呼叫的函數。 下面是使用 Fl_Button 與回呼函數 (callback function) 的程式,還示範了應該如何設置 shortcut。

#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Window.H>
#include <cstdlib>

void close_callback(Fl_Widget *, void *) {
    if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
        return;

    exit(0);
}

int main(int argc, char **argv) {
    Fl_Window *window = new Fl_Window(340, 180);

    Fl_Button *button = new Fl_Button(20, 40, 300, 100, "Hello, World!");
    button->type(FL_NORMAL_BUTTON);
    button->color(FL_WHITE);
    button->labelcolor(FL_BLACK);    
    button->shortcut(FL_ALT + 'q');
    button->callback(close_callback);

    window->end();
    window->callback(close_callback);
    window->show(argc, argv);
    return Fl::run();
}

通常只有當視窗元件的值發生變化時才會執行回呼函數,使用者可以使用 Fl_Widget::when() 方法進行相關的設定。

FLTK 也可以使用 Lambda Expression 作為回呼函數,下面是改寫的版本:

#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Window.H>
#include <cstdlib>

int main(int argc, char **argv) {
    auto lambda = [](Fl_Widget *, void *) -> void {
        if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
            return;

        exit(0);
    };

    Fl_Window *window = new Fl_Window(340, 180);

    Fl_Button *button = new Fl_Button(20, 40, 300, 100, "Hello, World!");
    button->type(FL_NORMAL_BUTTON);
    button->color(FL_WHITE);
    button->labelcolor(FL_BLACK);
    button->shortcut(FL_ALT + 'q');
    button->callback(lambda);

    window->end();
    window->callback(lambda);
    window->show(argc, argv);
    return Fl::run();
}

下面是使用了 Fl_Check_Button 與 Fl_Output 的程式:

#include <FL/Fl.H>
#include <FL/Fl_Check_Button.H>
#include <FL/Fl_Output.H>
#include <FL/Fl_Window.H>

int main(int argc, char **argv) {
    Fl::scheme("gtk+");

    auto lambda = [](Fl_Widget *w, void *data) -> void {
        Fl_Check_Button *button = static_cast<Fl_Check_Button *>(w);
        Fl_Output *output = static_cast<Fl_Output *>(data);

        if (button->value()) {
            output->value("Checkbox is ON");
            output->redraw();
        } else {
            output->value("Checkbox is OFF");
            output->redraw();
        }
    };

    Fl_Window *window = new Fl_Window(350, 250, "FLTK Example");

    Fl_Check_Button *check_button =
        new Fl_Check_Button(50, 50, 150, 30, "Enable Feature");

    Fl_Output *output = new Fl_Output(50, 100, 200, 30);
    output->align(FL_ALIGN_BOTTOM);
    output->tooltip("Display checkbox status.");
    output->value("Checkbox is OFF");

    check_button->callback(lambda, output);

    window->end();
    window->show(argc, argv);
    return Fl::run();
}

下面是使用了 Fl_Round_Button 的程式:

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Round_Button.H>
#include <FL/Fl_Window.H>

void button_cb(Fl_Widget *w, void *data) {
    Fl_Round_Button *button = static_cast<Fl_Round_Button *>(w);
    const char *label = button->label();

    Fl_Box *output_box = static_cast<Fl_Box *>(data);
    if (button->value()) {
        output_box->copy_label(label);
    }
    output_box->redraw();
}

int main(int argc, char **argv) {
    Fl::scheme("gtk+");

    Fl_Window *window = new Fl_Window(300, 200, "Example");

    Fl_Box *output_box = new Fl_Box(20, 150, 260, 30, "Selected: None");
    output_box->box(FL_FLAT_BOX);
    output_box->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
    output_box->labelcolor(FL_BLUE);

    Fl_Round_Button *button1 = new Fl_Round_Button(20, 20, 100, 30, "Option A");
    button1->type(FL_RADIO_BUTTON);
    button1->callback(button_cb, output_box);

    Fl_Round_Button *button2 = new Fl_Round_Button(20, 60, 100, 30, "Option B");
    button2->type(FL_RADIO_BUTTON);
    button2->callback(button_cb, output_box);

    Fl_Round_Button *button3 =
        new Fl_Round_Button(20, 100, 100, 30, "Option C");
    button3->type(FL_RADIO_BUTTON);
    button3->callback(button_cb, output_box);

    window->end();
    window->show(argc, argv);
    return Fl::run();
}

下面是使用了 Fl_Slider 的程式:

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Slider.H>
#include <FL/Fl_Window.H>
#include <iomanip>
#include <sstream>
#include <string>

void slider_callback(Fl_Widget *w, void *data) {
    Fl_Slider *slider = static_cast<Fl_Slider *> (w);
    double value = slider->value();

    std::stringstream stream;
    stream << std::fixed << std::setprecision(2) << value;
    std::string myvalue = stream.str();

    Fl_Box *box = static_cast<Fl_Box *> (data);
    box->copy_label(myvalue.c_str()); // Update label
    box->redraw(); // Update UI
}

int main() {
    Fl::scheme("gtk+");
    Fl_Window *window = new Fl_Window(400, 250, "Example");

    Fl_Slider *slider = new Fl_Slider(50, 50, 300, 40);
    slider->type(FL_HORIZONTAL);
    slider->value(50.0);
    slider->range(0.0, 100.0);
    slider->step(0.5);

    std::stringstream stream;
    stream << std::fixed << std::setprecision(2) << slider->value();
    std::string myvalue = stream.str();

    Fl_Box *value_box = new Fl_Box(50, 150, 300, 30);
    value_box->box(FL_DOWN_BOX);
    value_box->label(myvalue.c_str());

    slider->callback(slider_callback, value_box);

    window->end();
    window->show();
    return Fl::run();
}

下面是使用了 Fl_Choice 的程式:

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Choice.H>
#include <FL/Fl_Window.H>
#include <FL/fl_ask.H>

void choice_callback(Fl_Widget *w, void *data) {
    Fl_Choice *choice = static_cast<Fl_Choice *>(w);
    Fl_Box *box = static_cast<Fl_Box *>(data);

    int selected_index = choice->value();
    const char *selected_text = choice->text(selected_index);

    box->copy_label(selected_text);
}

void button_callback(Fl_Widget *w, void *data) {
    Fl_Choice *choice = static_cast<Fl_Choice *> (data);
    int selected_index = choice->value();
    const char *selected_text = choice->text(selected_index);

    // Display the selection in an alert box
    fl_message("You selected: %s (Index: %d)", selected_text, selected_index);
}

int main(int argc, char **argv) {
    Fl::scheme("plastic");

    Fl_Window *win = new Fl_Window(400, 300, "Fl_Choice Example");
    win->begin();

    Fl_Choice *choice = new Fl_Choice(100, 50, 150, 30, "Option:");
    choice->add("Ada");
    choice->add("C#");
    choice->add("C++");
    choice->add("Erlang");
    choice->add("Fortran");
    choice->add("Lua");
    choice->add("Java");
    choice->add("JavaScript");
    choice->add("Object Pascal");
    choice->add("Perl");
    choice->add("PHP");
    choice->add("Python");
    choice->add("Rexx");
    choice->add("Tcl");

    Fl_Box *box = new Fl_Box(100, 100, 150, 30);
    box->box(FL_UP_BOX);
    choice->callback(choice_callback, box);

    Fl_Button *button = new Fl_Button(100, 150, 150, 30, "Show Selection");
    button->callback(button_callback, choice);

    win->end();
    win->show(argc, argv);

    return Fl::run();
}

Fl_Browser 視窗元件顯示一個可捲動的文字行列表,並管理所有文字的儲存。下面是修改自 FLTK 範例程式的練習。

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Multi_Browser.H>

// Hold Browser's callback
void HoldBrowserCallback(Fl_Widget *w, void *data) {
    Fl_Hold_Browser *brow = static_cast<Fl_Hold_Browser *>(w);
    Fl_Box *box = static_cast<Fl_Box *>(data);

    int line = brow->value();
    box->copy_label(brow->text(line));
}

int main(int argc, char *argv[]) {
    Fl::scheme("gtk+");
    Fl_Double_Window *win = new Fl_Double_Window(360, 270, "Simple Browser");
    win->begin();

    Fl_Hold_Browser *brow = new Fl_Hold_Browser(10, 10, win->w() - 20, 100);
    brow->callback(HoldBrowserCallback); // callback for hold browser
    brow->add("One");
    brow->add("Two");
    brow->add("Three");
    brow->add("Four");
    brow->select(1);

    Fl_Box *box = new Fl_Box(10, 120, win->w() - 20, 100, "One");
    box->box(FL_UP_BOX);
    box->labelfont(FL_BOLD + FL_ITALIC);
    box->labelsize(36);
    box->labelcolor(FL_BLUE);

    brow->callback(HoldBrowserCallback, box); // callback for hold browser

    win->end();
    win->resizable(win);
    win->show(argc, argv);
    return (Fl::run());
}

下面是一個簡單的 right-click context menu 例子。

#include <FL/Fl.H>
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl_Multiline_Input.H>
#include <FL/Fl_Window.H>
#include <cstdlib>

void Menu_CB(Fl_Widget *w, void *d) {
    exit(0);
}

int main(int argc, char **argv) {
    Fl_Window *window = new Fl_Window(640, 480);
    window->tooltip("Use right-click for popup menu...");

    Fl_Menu_Button *menu = new Fl_Menu_Button(0, 0, 640, 480, "Popup Menu");
    menu->type(Fl_Menu_Button::POPUP3);  // pops menu on right click
    menu->add("Quit", "^q", Menu_CB, 0); // ctrl-q hotkey
    menu->menu_end();

    Fl_Multiline_Input *input =
        new Fl_Multiline_Input(100, 200, 350, 50, "Input");
    input->value("Right-click anywhere on gray window area\nfor popup menu");

    window->end();
    window->show(argc, argv);
    return Fl::run();
}

下面是一個簡單的 Fl_Menu_Bar 例子。

#include <FL/Fl.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Window.H>
#include <cstdio>
#include <cstdlib>
#include <cstring>

static void MyMenuCallback(Fl_Widget *w, void *) {
    Fl_Menu_Bar *bar = static_cast<Fl_Menu_Bar *> (w);
    const Fl_Menu_Item *item = bar->mvalue();

    char ipath[256];
    bar->item_pathname(ipath, sizeof(ipath));
    fprintf(stderr, "callback: You picked '%s'",
            item->label());                              // Print item picked
    fprintf(stderr, ", item_pathname() is '%s'", ipath); // ..and full pathname

    fprintf(stderr, "\n");
    if (strcmp(item->label(), "&Quit") == 0) {
        exit(0);
    }
}

int main() {
    Fl::scheme("gleam");
    Fl_Window *win = new Fl_Window(640, 480, "menubar");

    Fl_Menu_Bar *menu = new Fl_Menu_Bar(0, 0, 640, 25);
    menu->add("&File/&Quit", "^q", MyMenuCallback);
    menu->add("&Help/&About", 0, MyMenuCallback);

    win->end();
    win->show();
    return (Fl::run());
}

下面是修改自 Fl_Chart 範例,以及使用 FL_MENU_RADIO flag 的 Fl_Menu_Bar 的測試程式。

#include <FL/Fl.H>
#include <FL/Fl_Chart.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Menu_Bar.H>
#include <cmath>
#include <cstdio>

Fl_Double_Window *g_win = nullptr;
Fl_Menu_Bar *g_menubar = nullptr;
Fl_Chart *g_chart = nullptr;

static void MyMenuCallback(Fl_Widget *w, void *data) {
    Fl_Menu_Bar *bar = static_cast<Fl_Menu_Bar *>(w);
    const Fl_Menu_Item *item = bar->mvalue();

    if (item->flags & FL_MENU_RADIO) {
        g_chart->type((uchar)item->argument()); // apply change
        g_chart->redraw();
    }
}

int main(int argc, char **argv) {
    g_win = new Fl_Double_Window(720, 486);

    g_menubar = new Fl_Menu_Bar(0, 0, g_win->w(), 25);
    g_menubar->add("Options/FL_BAR_CHART", 0, MyMenuCallback,
                   (void *)FL_BAR_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_HORBAR_CHART", 0, MyMenuCallback,
                   (void *)FL_HORBAR_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_LINE_CHART", 0, MyMenuCallback,
                   (void *)FL_LINE_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_FILL_CHART", 0, MyMenuCallback,
                   (void *)FL_FILL_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_SPIKE_CHART", 0, MyMenuCallback,
                   (void *)FL_SPIKE_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_PIE_CHART", 0, MyMenuCallback,
                   (void *)FL_PIE_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_SPECIALPIE_CHART", 0, MyMenuCallback,
                   (void *)FL_SPECIALPIE_CHART, FL_MENU_RADIO);

    {
        Fl_Menu_Item *item =
            (Fl_Menu_Item *)g_menubar->find_item("Options/FL_BAR_CHART");
        item->value(1);
    }

    g_chart = new Fl_Chart(20, 40, g_win->w() - 40, g_win->h() - 80, "Chart");
    g_chart->bounds(-125.0, 125.0);
    const double start = 1.5;
    const double end = start + 15.1;
    for (double t = start; t < end; t += 0.5) {
        double val = sin(t) * 125.0;
        static char val_str[20];
        sprintf(val_str, "%.0lf", val);
        g_chart->add(val, val_str, (val < 0) ? FL_RED : FL_GREEN);
    }

    g_win->end();
    g_win->resizable(g_win);
    g_win->show();

    return (Fl::run());
}

Fl_Terminal 是 FLTK 1.4 系列新增的視窗元件,用來提供一個可滾動的顯示區域來模擬終端 (Terminal)。 下面是我修改 Fl_Terminal 範例程式的練習。

#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Terminal.H>
#include <time.h>

#define TERMINAL_HEIGHT 550

// Globals
Fl_Double_Window *g_win = nullptr;
Fl_Box *g_msg = nullptr;
Fl_Button *g_button = nullptr;
Fl_Terminal *g_tty = nullptr;
Fl_Box *status_bar = nullptr;

// Append a date/time message to the terminal every 1 seconds
void tick_cb(void *data) {
    time_t lt = time(NULL);
    g_tty->printf("Timer tick: \033[32m%s\033[0m\n", ctime(&lt));
    Fl::repeat_timeout(1.0, tick_cb, data);
}

void button_cb(Fl_Widget *w, void *data) {
    Fl_Button *button = static_cast<Fl_Button *>(w);

    int status = Fl::has_timeout(tick_cb);
    if (status == 0) {
        Fl::add_timeout(0.5, tick_cb);
        status_bar->label("Running.");
    }
}

int main(int argc, char **argv) {
    Fl::scheme("gtk+");
    g_win = new Fl_Double_Window(840, 630, "Example");
    g_win->begin();

    g_msg = new Fl_Box(50, 10, g_win->w() - 150, 30, "Fl_Terminal example");
    g_msg->labelcolor(FL_BLUE);
    g_button = new Fl_Button(g_win->w() - 80, 10, 60, 30, "Run");
    g_button->callback(button_cb);

    g_tty = new Fl_Terminal(0, 50, g_win->w(), TERMINAL_HEIGHT);
    g_tty->ansi(true);

    // Creating a status bar in FLTK by using Fl_Box
    status_bar = new Fl_Box(0, g_win->h() - 30, g_win->w(), 30);
    status_bar->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
    status_bar->box(FL_DOWN_BOX);
    status_bar->label("Ready.");

    g_win->end();
    g_win->resizable(g_win);
    g_win->show(argc, argv);
    return Fl::run();
}

下面是我的另外一個 Fl_Terminal 練習程式,使用 Fl_Input 取得使用者的輸入,而後使用 popen() 嘗試執行。

#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Terminal.H>
#include <FL/fl_ask.H>
#include <cstdio>
#include <cstdlib>
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#endif

#define TERMINAL_HEIGHT 530

// Globals
Fl_Double_Window *g_win = nullptr;
Fl_Menu_Bar *g_menu = nullptr;
Fl_Input *g_input = nullptr;
Fl_Button *g_button = nullptr;
Fl_Terminal *g_tty = nullptr;

void MyMenuCallback(Fl_Widget *w, void *) {
    Fl_Menu_Bar *bar = static_cast<Fl_Menu_Bar *>(w);
    const Fl_Menu_Item *item = bar->mvalue();

    if (strcmp(item->label(), "&Quit") == 0) {
        exit(0);
    } else if (strcmp(item->label(), "&About") == 0) {
        fl_message_title("About");
        fl_message("Welcome.\n"
                   "It is a FLTK program to test Fl_Terminal widget.");
    }
}

void button_cb(Fl_Widget *w, void *data) {
    Fl_Button *button = static_cast<Fl_Button *>(w);

    char text[1024];
    sprintf(text, "%s 2>&1", g_input->value()); // stderr + stdout
    g_tty->printf("\nEXECUTING: %s\n", text);

    // The popen() function opens a process by creating a pipe, forking,
    // and invoking the shell.
    FILE *fp = popen(text, "r");
    if (fp == 0) {
        g_tty->printf("Failed to execute: '%s'\n", text);
    } else {
        char s[1024];
        while (fgets(s, sizeof(s) - 1, fp)) {
            g_tty->printf("%s", s);
        }
        pclose(fp);
    }
}

int main(int argc, char **argv) {
    Fl::scheme("gleam");
    g_win = new Fl_Double_Window(840, 630, "Example");
    g_win->begin();

    g_menu = new Fl_Menu_Bar(0, 0, g_win->w(), 30);
    g_menu->add("&File/&Quit", "^q", MyMenuCallback);
    g_menu->add("&Help/&About", 0, MyMenuCallback);

    g_input = new Fl_Input(50, 40, g_win->w() - 150, 30, "Input: ");
    g_input->maximum_size(1000);
    g_button = new Fl_Button(g_win->w() - 80, 40, 60, 30, "Run");
    g_button->callback(button_cb);

    g_tty = new Fl_Terminal(0, 90, g_win->w(), TERMINAL_HEIGHT);
    g_tty->ansi(true);

    g_win->end();
    g_win->resizable(g_win);
    g_win->show(argc, argv);
    return Fl::run();
}

Layout and containers

Fl_Scroll, Fl_Tabs 與 Fl_Wizard 是所謂的容器 (containers),基本上是直接固定視窗元件的的佈局功能, 繼承 Fl_Group 的功能加上一些加強的能力。

下面是使用 Fl_Tabs 與 Fl_Widget::when() 方法的例子。 Fl_Widget::when() 設定用於決定何時呼叫 callback function 的標記。

#include <FL/Enumerations.H>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Tabs.H>
#include <FL/Fl_Window.H>

void tab_closed_cb(Fl_Widget *w, void *data) {
    auto parent = w->parent();
    parent->remove(w);
}

int main(int argc, char **argv) {
    Fl::scheme("gleam");
    Fl_Window window(Fl::w() / 2, Fl::h() / 2, "test");
    Fl_Box windowBox(0, 32, window.w(), window.h() - 32);
    window.resizable(&windowBox);
    Fl_Tabs mainTabs(0, 32, window.w(), window.h() - 64);

    // First tab
    Fl_Group tab1(0, 64, window.w(), window.h() - 64, "Tab 1");
    Fl_Button *button1 = new Fl_Button(20, 100, 100, 25, "Button 1");
    Fl_Box *box1 = new Fl_Box(20, 145, 100, 50, "Text display");
    tab1.when(FL_WHEN_CLOSED);     // Let user can remove this tab
    tab1.callback(tab_closed_cb);
    tab1.end();

    // Second tab
    Fl_Group tab2(0, 64, window.w(), window.h() - 64, "Tab 2");
    Fl_Button *button2 = new Fl_Button(20, 100, 100, 25, "Button 2");
    Fl_Input *input = new Fl_Input(65, 145, 250, 25, "Input:");
    input->align(FL_ALIGN_LEFT);
    tab2.when(FL_WHEN_CLOSED);
    tab2.callback(tab_closed_cb);
    tab2.end();

    // Third tab
    Fl_Group tab3(0, 64, window.w(), window.h() - 64, "Tab 3");
    Fl_Button *button3 = new Fl_Button(20, 100, 100, 25, "Button 3");
    tab3.when(FL_WHEN_CLOSED);
    tab3.callback(tab_closed_cb);
    tab3.end();

    mainTabs.end();
    window.end();
    window.show(argc, argv);
    return Fl::run();
}

除了直接固定視窗元件的位置,FLTK 提供了一些佈局用的視窗元件可以用來作為自動佈局視窗元件的位置。

下面是 Fl_Flex 的例子。

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Flex.H>
#include <FL/fl_ask.H>

void exit_cb(Fl_Widget *w, void *) {
    fl_message("The '%s' button closes the window\nand terminates the program.",
               w->label());
    w->window()->hide();
}

void button_cb(Fl_Widget *w, void *) {
    fl_message("The '%s' button does nothing.", w->label());
}

int main(int argc, char **argv) {
    Fl_Double_Window window(640, 60, "Simple Fl_Flex Demo");
    Fl_Flex flex(5, 5, window.w() - 10, window.h() - 10, Fl_Flex::HORIZONTAL);
    Fl_Button b1(0, 0, 0, 0, "File");
    Fl_Button b2(0, 0, 0, 0, "New");
    Fl_Button b3(0, 0, 0, 0, "Save");
    Fl_Box bx(0, 0, 0, 0); // empty space
    Fl_Button eb(0, 0, 0, 0, "Exit");

    // assign callbacks to buttons
    b1.callback(button_cb);
    b2.callback(button_cb);
    b3.callback(button_cb);
    eb.callback(exit_cb);

    // set gap between adjacent buttons and extra spacing (invisible box size)
    flex.gap(10);
    flex.fixed(bx, 30);

    // end() groups
    flex.end();
    window.end();

    // set resizable, minimal window size, show() window, and execute event loop
    window.resizable(flex);
    window.size_range(300, 30);
    window.show(argc, argv);
    return Fl::run();
}

Fl_Tile 類別可以讓使用者透過拖曳子元件之間的邊框來調整其子元件的大小。下面是使用的例子:

#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Tile.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Pack.H>
#include <FL/fl_draw.H>
#include <cstdlib>

void close_callback(Fl_Widget *, void *) {
    if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
        return;

    exit(0);
}

// A custom box with a label and color
class ColorBox : public Fl_Box {
public:
    ColorBox(int x, int y, int w, int h, const char* label, Fl_Color c)
        : Fl_Box(x, y, w, h, label) {
        box(FL_FLAT_BOX);
        labelcolor(FL_BLACK);
        labelsize(24);
        labelfont(FL_BOLD);
        color(c);
    }
};

int main(int argc, char **argv) {
    Fl_Double_Window* window = new Fl_Double_Window(600, 400, "Fl_Tile Example");
    window->begin();

    Fl_Tile* tile = new Fl_Tile(0, 0, 600, 400);
    tile->begin();

    ColorBox* box1 = new ColorBox(0, 0, 200, 400, "Left", FL_YELLOW);
    ColorBox* box2 = new ColorBox(200, 0, 400, 200, "Top Right", FL_GREEN);
    ColorBox* box3 = new ColorBox(200, 200, 400, 200, "Bottom Right", FL_CYAN);

    tile->end();

    window->resizable(tile);
    window->callback(close_callback);
    window->show(argc, argv);
    return Fl::run();
}

下面是 Fl_Grid 的例子。

#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Grid.H>
#include <cstdlib>

int main(int argc, char **argv) {
    Fl_Double_Window *win =
        new Fl_Double_Window(320, 180, "3x3 Fl_Grid with Buttons");

    // create the Fl_Grid container with five buttons
    Fl_Grid *grid = new Fl_Grid(0, 0, win->w(), win->h());
    grid->layout(3, 3, 10, 10);
    grid->color(FL_WHITE);

    Fl_Button *b0 = new Fl_Button(0, 0, 0, 0, "New");
    Fl_Button *b1 = new Fl_Button(0, 0, 0, 0, "Options");
    Fl_Button *b3 = new Fl_Button(0, 0, 0, 0, "About");
    Fl_Button *b4 = new Fl_Button(0, 0, 0, 0, "Help");
    Fl_Button *b6 = new Fl_Button(0, 0, 0, 0, "Quit");
    b6->color(FL_BLACK);
    b6->labelcolor(FL_WHITE);
    b6->callback([](Fl_Widget *, void *) -> void { exit(0); });

    // assign buttons to grid positions
    grid->widget(b0, 0, 0);
    grid->widget(b1, 0, 1, 1, 2);
    grid->widget(b3, 1, 1);
    grid->widget(b4, 2, 0);
    grid->widget(b6, 2, 2);
    // grid->show_grid(1);     // enable to display grid helper lines
    grid->end();

    win->end();
    win->resizable(grid);
    win->size_range(300, 100);
    win->show(argc, argv);
    return Fl::run();
}

Drawing

只有在某些特定的地方才可以執行繪圖到螢幕顯示器的 FLTK 程式碼。在其他地方呼叫這些函數將會導致未定義的行為!

  • 最常見的地方是在 Fl_Widget::draw() 方法內部。要在此處編寫程式碼, 必須將現有 Fl_Widget 類別之一子類化並實作自己的 draw() 版本。
  • 也可以建立自訂 boxtypes 與 labeltypes。這些涉及編寫可由現有 Fl_Widget::draw() 方法呼叫的小程序。 這些「類型」由儲存在 widget 的 box(), labeltype() 和可能的其他屬性中的 8 位元索引標識。
  • 可以呼叫 Fl_Window::make_current() 來對視窗元件進行增量更新。使用 Fl_Widget::window() 來尋找視窗。

下面的程式是畫一個 X 的示範程式。

#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/fl_draw.H>

class DrawX : public Fl_Widget {
public:
    DrawX(int X, int Y, int W, int H, const char *L = 0)
        : Fl_Widget(X, Y, W, H, L) {}

    virtual void draw() FL_OVERRIDE {
        fl_color(FL_YELLOW);
        fl_rectf(x(), y(), w(), h());
        fl_color(FL_BLUE);
        int x1 = x(), y1 = y();
        int x2 = x() + w() - 1, y2 = y() + h() - 1;
        fl_line(x1, y1, x2, y2);
        fl_line(x1, y2, x2, y1);
    }
};

int main() {
    Fl_Double_Window win(400, 400, "Draw X");
    DrawX draw_x(10, 10, win.w() - 20, win.h() - 20);
    draw_x.color(FL_YELLOW);
    win.resizable(draw_x);
    win.show();
    return (Fl::run());
}

Event

每次使用者移動滑鼠指標、點擊按鈕或按下按鍵時,都會產生一個事件 (event) 並發送到使用者的應用程式。 事件也可以來自其他程序,例如視窗管理器 (window manager)。

在 FLTK 中事件由傳遞給 handle() 方法的整數參數標識,該方法覆蓋 Fl_Widget::handle() virtual method。 有關最近事件的其他資訊儲存在靜態位置,並透過呼叫獲取 Fl::event_∗() 方法。 此靜態資訊一直有效,直到從視窗系統讀取下一個事件為止,因此可以在 handle() 方法之外查看它,例如在 callbacks 中。

下面是一個繼承 Fl_Window,並且在 handle() 方法印出事件名稱的例子。

#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Window.H>
#include <FL/names.h> // FLTK 1.1.8 and up
#include <cstdlib>

class PrintEevent_Window : public Fl_Window {
    int handle(int e) {
        fprintf(stderr, "EVENT: %s(%d)\n", fl_eventnames[e], e);
        return (Fl_Window::handle(e));
    }

public:
    PrintEevent_Window(int W, int H, const char *L = 0) : Fl_Window(W, H, L) {
    }
};

void close_callback(Fl_Widget *, void *) {
    if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
        return;

    exit(0);
}

int main(int argc, char **argv) {
    PrintEevent_Window *window = new PrintEevent_Window(340, 180);

    Fl_Button *button = new Fl_Button(20, 40, 300, 100, "Exit");
    button->type(FL_NORMAL_BUTTON);
    button->color(FL_WHITE);
    button->labelcolor(FL_BLACK);
    button->shortcut(FL_ALT + 'q');
    button->callback(close_callback);

    window->end();
    window->callback(close_callback);
    window->show(argc, argv);
    return Fl::run();
}

再來改寫上面的程式,使用 FL_MOVE 事件取得目前滑鼠指標的位置,並且顯示在標題列。

#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Window.H>
#include <FL/names.h> // FLTK 1.1.8 and up
#include <cstdlib>
#include <sstream>
#include <string>

class MouseEvent_Window : public Fl_Window {
    int handle(int e) {
        std::stringstream stream;
        std::string myvalue;

        switch (e) {
        case FL_MOVE:
            stream << "(" << Fl::event_x() << ", " << Fl::event_y() << ")";
            myvalue = stream.str();
            copy_label(myvalue.c_str());

            break;
        default:
            break;
        }

        return (Fl_Window::handle(e));
    }

public:
    MouseEvent_Window(int W, int H, const char *L = 0) : Fl_Window(W, H, L) {}
};

void close_callback(Fl_Widget *, void *) {
    if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
        return;

    exit(0);
}

int main(int argc, char **argv) {
    MouseEvent_Window *window = new MouseEvent_Window(340, 180);

    Fl_Button *button = new Fl_Button(20, 40, 300, 100, "Exit");
    button->type(FL_NORMAL_BUTTON);
    button->color(FL_WHITE);
    button->labelcolor(FL_BLACK);
    button->shortcut(FL_ALT + 'q');
    button->callback(close_callback);

    window->end();
    window->callback(close_callback);
    window->show(argc, argv);
    return Fl::run();
}

OpenGL

FLTK 提供了 Fl_Gl_Window,可以繼承此類別並且在 draw() 函式繪制自己需要的內容。

#include <FL/Fl.H>
#include <FL/Fl_Gl_Window.H>
#include <FL/gl.h>
#include <cstdlib>

class MyGlWindow : public Fl_Gl_Window {
    void draw() {
        if (!valid()) {
            glLoadIdentity();
            glViewport(0, 0, w(), h());
            glOrtho(-w(), w(), -h(), h(), -1, 1);
        }

        glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        glColor3f(0.0, 0.0, 0.0);
        glBegin(GL_LINE_STRIP);
        glVertex2f(w(), h());
        glVertex2f(-w(), -h());
        glEnd();
        glBegin(GL_LINE_STRIP);
        glVertex2f(w(), -h());
        glVertex2f(-w(), h());
        glEnd();
    }

public:
    MyGlWindow(int X, int Y, int W, int H, const char *L = 0)
        : Fl_Gl_Window(X, Y, W, H, L) {}
};

int main(int argc, char **argv) {
    Fl_Window win(640, 480, "OpenGL sample");
    MyGlWindow mygl(10, 10, win.w() - 20, win.h() - 20);
    win.end();
    win.callback([](Fl_Widget *, void *) {
        if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
            return;

        exit(0);
    });

    win.resizable(mygl);
    win.show(argc, argv);
    return (Fl::run());
}

CMakeLists.txt 需要增加 FLTK OpenGL 的部份:

target_link_libraries(sample PRIVATE fltk::fltk fltk::gl)

我在測試的時候不需要加入尋找 OpenGL 的部份,如果發現有相關的錯誤而需要加入,首先在 CMakeLists.txt 加入 find_package:

find_package(OpenGL REQUIRED)

MESSAGE ( STATUS "  OPENGL_FOUND :        " ${OPENGL_FOUND} )
MESSAGE ( STATUS "  OPENGL_INCLUDE_PATH:  " ${OPENGL_INCLUDE_PATH} )

更新 target_include_directories,然後在連結的部份加入:

target_link_libraries(hello PRIVATE fltk::fltk fltk::gl OpenGL::GL)

相關連結

2025/03/25

C++ Thread

Process and Thread

行程 (Process) 和執行緒 (Thread) 之間的不同點:

  • Process:
    A process is a program placed in memory or running with all the run-time environment (or all the resources) associated with it.
  • Thread:
    A thread is a portion or part of the process and the smallest sequential unit of instructions processed independently by the scheduler of the operating system.
    It is simply an execution or processing unit composed of a set of instructions contained in the process.

Process 之間的資源是隔離的,而同一個 Process 內的 Thread 之間的記憶體資源是共用的,所以使用 Thread 需要小心的處理全域變數, 需要使用互斥鎖(Mutex)等完成執行緒之間的同步管理。 除了共同的記憶體,執行緒區域儲存區(Thread Local Storage,TLS) 可以讓執行緒有私人的資料儲存區,讓執行緒可以儲存各自執行緒的資料。

Windows 平台使用 CreateProcess() 建立一個新的行程。Unix-like 平台需要先使用 fork() 建立一個新的行程 (Process), 再使用 execvp() 或者 exec 系列的函數替換為新程式。 以下 C 程式示範在 Unix-like 平台上如何使用 fork() 建立一個新的行程 (Process), 並在子行程中使用 execvp() 替換為新程式(本例中為 ls -al)。父程序使用 wait() 暫停其執行,直到子程序完成。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid;
    int status;

    pid = fork();

    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // This is the child process
        printf("Child process: My PID is %u, my parent's PID is %u\n", getpid(),
               getppid());

        char *argv_list[] = {"ls", "-al", NULL};

        execvp("ls", argv_list);

        // If execvp() is successful, the code that follows it in the child
        // process is never executed because the original process is gone.
        perror("execvp failed");
        exit(EXIT_FAILURE);
    } else {
        // This is the parent process
        printf("Parent process: My PID is %u, my child's PID is %u\n", getpid(),
               pid);

        if (waitpid(pid, &status, 0) == -1) {
            perror("waitpid failed");
        } else {
            printf("Parent process: Child %u finished with status %d\n", pid,
                   status);
        }
        exit(EXIT_SUCCESS);
    }

    return 0;
}

Pthreads

POSIX Threads 是 POSIX 的執行緒標準,定義了建立和操控執行緒的一套API。

Pthreads API 全都以 "pthread_" 開頭,並可以分為四類:

  • 執行緒管理,例如建立執行緒,等待(join)執行緒,查詢執行緒狀態等。
  • 互斥鎖(Mutex):建立、摧毀、鎖定、解鎖、設定屬性等操作
  • 條件變數(Condition Variable):建立、摧毀、等待、通知、設定與查詢屬性等操作
  • 使用了互斥鎖的執行緒間的同步管理

下面是使用的例子:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static void wait(void) {
    time_t start_time = time(NULL);

    while (time(NULL) == start_time) {
        /* do nothing except chew CPU slices for up to one second */
    }
}

static void *thread_func(void *vptr_args) {
    int i;

    for (i = 0; i < 20; i++) {
        fputs("  b\n", stderr);
        wait();
    }

    return NULL;
}

int main(void) {
    int i;
    pthread_t thread;

    if (pthread_create(&thread, NULL, thread_func, NULL) != 0) {
        return EXIT_FAILURE;
    }

    for (i = 0; i < 20; i++) {
        puts("a");
        wait();
    }

    if (pthread_join(thread, NULL) != 0) {
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

建立執行緒的函式如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

執行緒屬性 pthread_attr_t 可以設定 __detachstate,表示新執行緒是否與行程中其他執行緒脫離同步。 如果設定為 PTHREAD_CREATE_DETACHED,則新執行緒不能用 pthread_join() 來同步,且在退出時自行釋放所占用的資源。 預設為 PTHREAD_CREATE_JOINABLE 狀態。也可以先建立執行緒並執行以後用 pthread_detach() 來設定。 一旦設定為 PTHREAD_CREATE_DETACHED 狀態,不論是建立時設定還是執行時設定,都不能再恢復到 PTHREAD_CREATE_JOINABLE 狀態。

C++ Thread

自 C++11 開始,C++ 標準函式庫提供了 Thread library。

下面就是一個使用的例子。

#include <iostream>
#include <thread>

void myfunc() { std::cout << "At myfunc..." << std::endl; }

int main() {
    std::thread t1(myfunc);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    t1.join();
    return 0;
}

參考連結

2024/11/22

PugiXML

PugiXML 是一個 C++ XML parser 函式庫,支援 DOM-like interface 與 XPATH 1.0 標準。 PugiXML 在是否容易使用、執行速度以及支援功能中取得良好的平衡,其中一個特點就是容易與其它程式整合, 將 pugixml.cpp, pugixml.hpp 與 pugiconfig.hpp 複製到原始碼目錄下就可以開始使用了。

下面是 tree.xml

<?xml version="1.0"?>
<mesh name="mesh_root">
    <!-- here is a mesh node -->
    some text
    <![CDATA[someothertext]]>
    some more text
    <node attr1="value1" attr2="value2" />
    <node attr1="value2">
        <innernode/>
    </node>
</mesh>
<?include somedata?>

下面是載入 XML 檔案的程式:

#include "pugixml.hpp"
#include <iostream>

int main() {
    pugi::xml_document doc;

    pugi::xml_parse_result result = doc.load_file("tree.xml");

    std::cout << "Load result: " << result.description()
              << ", mesh name: " << doc.child("mesh").attribute("name").value()
              << std::endl;
}

下面的程式使用 libcurl 自網站下載 ATOM XML 的資料, 下載以後使用 PugiXML 分析並且將 title 與 link 的資料儲存為 html 格式。

#include "pugixml.hpp"
#include <cstdio>
#include <cstdlib>
#include <curl/curl.h>
#include <fstream>
#include <iostream>

int get_rss(const char *url, const char *outfile) {
    FILE *feedfile = fopen(outfile, "w");

    if (!feedfile)
        return -1;

    CURL *curl = curl_easy_init();
    if (!curl)
        return -1;

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, feedfile);

    CURLcode res = curl_easy_perform(curl);
    if (res)
        return -1;
    curl_easy_cleanup(curl);
    fclose(feedfile);
    return 0;
}

int main(int argc, char *argv[]) {
    char *url = NULL;
    char *filename = NULL;
    char *outfile = NULL;
    if (argc == 4) {
        url = argv[1];
        filename = argv[2];
        outfile = argv[3];
    } else {
        printf("Not valid arguments.\n");
    }

    get_rss(url, filename);

    pugi::xml_document doc;
    pugi::xml_parse_result result = doc.load_file(filename);

    pugi::xpath_node_set title = doc.select_nodes("/feed/entry/title");
    pugi::xpath_node_set link =
        doc.select_nodes("/feed/entry/link[@rel='alternate']");

    std::ofstream ofile(outfile);

    pugi::xpath_node_set::const_iterator it1 = title.begin();
    pugi::xpath_node_set::const_iterator it2 = link.begin();
    while (it1 != title.end() && it2 != link.end()) {
        pugi::xpath_node node1 = *it1;
        pugi::xpath_node node2 = *it2;
        ofile << "<a href=\"" << node2.node().attribute("href").value() << "\">"
              << node1.node().text().get() << "</a><br>" << std::endl;

        it1++;
        it2++;
    }
    ofile.close();
}

參考連結

2024/04/17

OpenCL

簡介

OpenCL(Open Computing Language,開放計算語言)是一個為異構平台編寫程式的框架, 此異構平台可由 CPU、GPU、DSP、FPGA 或其他類型的處理器與硬體加速器所組成。 Portable Computing Language (PoCL) 則是 OpenCL 的一個自由軟體實作, 可以在機器上沒有 GPU 的情況下使用 OpenCL API 進行運算。

OpenCL 包括一組 API 和一個程式語言。基本的原理是程式透過 OpenCL API 取得 OpenCL 裝置(例如顯示晶片)的相關資料, 並將要在裝置上執行的程式(使用 OpenCL 程式語言撰寫)編繹成適當的格式以後在裝置上執行。

An OpenCL application is split into host code and device kernel code. Execution of an OpenCL program occurs in two parts: kernelsthat execute on one or more OpenCL devices and a host program that executes on the host.

The most commonly used language for programming the kernels that are compiled and executed across the available parallel processors is called OpenCL C. OpenCL C is based on C99 and is defined as part of the OpenCL specification.

The core of the OpenCL execution model is defined by how the kernels execute. OpenCL regards a kernel program as the basic unit of executable code (similar to a C function). Kernels can execute with data or task-parallelism. An OpenCL program is a collection of kernels and functions (similar to dynamic library with run-time linking).

An OpenCL command queue is used by the host application to send kernels and data transfer functions to a device for execution. By enqueueing commands into a command queue, kernels and data transfer functions may execute asynchronously and in parallel with application host code.

The kernels and functions in a command queue can be executed in-order or out-of-order. A compute device may have multiple command queues.


A complete sequence for executing an OpenCL program is:

  1. Query for available OpenCL platforms and devices
  2. Create a context for one or more OpenCL devices in a platform
  3. Create and build programs for OpenCL devices in the context
  4. Select kernels to execute from the programs
  5. Create memory objects for kernels to operate on
  6. Create command queues to execute commands on an OpenCL device
  7. Enqueue data transfer commands into the memory objects, if needed
  8. Enqueue kernels into the command queue for execution
  9. Enqueue commands to transfer data back to the host, if needed

A host is connected to one or more OpenCL compute devices. Each compute device is collection of one or more compute units where each compute unit is composed of one or more processing elements. Processing elements execute code with SIMD (Single Instruction Multiple Data) or SPMD (Single Program Multiple Data) parallelism.


For example, a compute device could be a GPU. Compute units would then correspond to the streaming multiprocessors (SMs) inside the GPU, and processing elements correspond to individual streaming processors (SPs) inside each SM. Processors typically group processing elements into compute units for implementation efficiency through sharing instruction dispatch and memory resources, and increasing local inter-processor communication.

OpenCL's clEnqueueNDRangeKernel command enables a single kernel program to be initiated to operate in parallel across an N-dimensional data structure. Using a two-dimensional image as a example, the size of the image would be the NDRange, and each pixel is called a work-item that a copy of kernel running on a single processing element will operate on.

As we saw in the Platform Model section above, it is common for processors to group processing elements into compute units for execution efficiency. Therefore, when using the clEnqueueNDRangeKernel command, the program specifies a work-group size that represents groups of individual work-items in an NDRange that can be accommodated on a compute unit. Work-items in the same work-group are able to share local memory, synchronize more easily using work-group barriers, and cooperate more efficiently using work-group functions such as async_work_group_copy that are not available between work-items in separate work-groups.


OpenCL has a hierarchy of memory types:

  • Host memory - available to the host CPU
  • Global/Constant memory - available to all compute units in a compute device
  • Local memory - available to all the processing elements in a compute unit
  • Private memory - available to a single processing element

OpenCL memory management is explicit. None of the above memories are automatically synchronized and so the application explicitly moves data between memory types as needed.


在 openSUSE Tumbleweed 上安裝 OpenCL 的開發檔案:

sudo zypper in ocl-icd-devel opencl-headers clinfo

安裝後執行 clinfo 檢查目前的 OpenCL 裝置資訊。
如果沒有符合的實作,為了學習 OpenCL, 可以安裝 Portable Computing Language (pocl):

sudo zypper in pocl-devel

OpenCL Installable Client Driver (ICD) allows multiple OpenCL implementations to co-exist; also, it allows applications to select between these implementations at runtime.

Use the clGetPlatformIDs() and clGetPlatformInfo() functions to see the list of available OpenCL implementations, and select the one that is best for your requirements.

執行 clinfo 觀察目前的 OpenCL device 資訊。

下面的程式使用 clGetPlatformIDs 函式取得目前可用的 platform 數目 (編譯指令:gcc test.c `pkg-config --libs --cflags OpenCL`):

#include <stdio.h>

#ifdef __APPLE__
#include <OpenCL/opencl.h>
#else
#include <CL/cl.h>
#endif

int main( void ) {
    // OpenCL related declarations
    cl_int err;
    cl_uint num;

    err = clGetPlatformIDs( 0, NULL, &num );
    printf("%d\n", num);

}

下面是另外一個範例:

#include <stdlib.h>
#include <stdio.h>

#ifdef __APPLE__
#include <OpenCL/opencl.h>
#else
#include <CL/cl.h>
#endif

const char *kernel_code =
    "__kernel void vector_add(__global const int *A, __global const int *B, __global int *C) {"
    "    int i = get_global_id(0);"
    "    C[i] = A[i] + B[i];"
    "}";

int main( void ) {
    // OpenCL related declarations
    cl_int err;
    cl_platform_id platform;
    cl_device_id device;
    cl_context_properties props[3] = { CL_CONTEXT_PLATFORM, 0, 0 };
    cl_context ctx;
    cl_program program;
    cl_command_queue queue;
    cl_kernel kernel;
    int i;

    //
    const size_t N = 1024; // vector size
    size_t global_item_size = N; // Process the entire lists
    size_t local_item_size = 64; // Divide work items into groups of 64

    int *A, *B, *C;
    A = (int*) malloc(N * sizeof(*A));
    B = (int*) malloc(N * sizeof(*B));
    C = (int*) malloc(N * sizeof(*C));
    for (i=0; i<N; i++) {
        A[i] = i;
        B[i] = i + 1;
    }
    cl_mem d_A, d_B, d_C;

    /* Setup OpenCL environment. */
    err = clGetPlatformIDs( 1, &platform, NULL );
    err = clGetDeviceIDs( platform, CL_DEVICE_TYPE_DEFAULT, 1, &device, NULL );

    props[1] = (cl_context_properties)platform;
    ctx = clCreateContext( props, 1, &device, NULL, NULL, &err );
    queue = clCreateCommandQueueWithProperties( ctx, device, 0, &err );
    program = clCreateProgramWithSource(ctx, 1, (const char **) &kernel_code, NULL, &err);
    err = clBuildProgram(program, 0, NULL, NULL, NULL, NULL);
    kernel = clCreateKernel(program, "vector_add", &err);

    // initialize buffer with data
    d_A = clCreateBuffer( ctx, CL_MEM_READ_ONLY, N*sizeof(*A), NULL, &err );
    d_B = clCreateBuffer( ctx, CL_MEM_READ_ONLY, N*sizeof(*B), NULL, &err );
    d_C = clCreateBuffer( ctx, CL_MEM_WRITE_ONLY, N*sizeof(*C), NULL, &err );

    err = clEnqueueWriteBuffer( queue, d_A, CL_TRUE, 0, N*sizeof(*A), A, 0, NULL, NULL );
    err = clEnqueueWriteBuffer( queue, d_B, CL_TRUE, 0, N*sizeof(*B), B, 0, NULL, NULL );

    err = clSetKernelArg(kernel, 0, sizeof(cl_mem), (void *)&d_A);
    err = clSetKernelArg(kernel, 1, sizeof(cl_mem), (void *)&d_B);
    err = clSetKernelArg(kernel, 2, sizeof(cl_mem), (void *)&d_C);

    err = clEnqueueNDRangeKernel(queue, kernel, 1, NULL,
            &global_item_size, &local_item_size, 0, NULL, NULL);

    err = clFinish(queue);

    err = clEnqueueReadBuffer( queue, d_C, CL_TRUE, 0, N*sizeof(*C), C, 0, NULL, NULL );
    err = clFinish(queue);

    for(i = 0; i < N; i++)
        printf("%d + %d = %d\n", A[i], B[i], C[i]);

    err = clFlush(queue);
    err = clFinish(queue);

    /* Release OpenCL memory objects. */
    clReleaseMemObject( d_A );
    clReleaseMemObject( d_B );
    clReleaseMemObject( d_C );
    free(A);
    free(B);
    free(C);
    clReleaseKernel( kernel );
    clReleaseProgram( program );
    clReleaseCommandQueue( queue );
    clReleaseContext( ctx );

    return 0;
}

下面的程式是使用 stb_image 讀取圖檔,測試 image object 功能的程式。

#include <stdio.h>
#include <stdlib.h>

#ifdef __APPLE__
#include <OpenCL/opencl.h>
#else
#include <CL/cl.h>
#endif

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

const char *kernel_code =
    "__kernel void PixelAccess(__read_only image2d_t imageIn,__write_only image2d_t imageOut)"
    "{"
    "  sampler_t srcSampler = CLK_NORMALIZED_COORDS_FALSE | "
    "    CLK_ADDRESS_CLAMP_TO_EDGE |"
    "    CLK_FILTER_NEAREST;"
    "  int2 imageCoord = (int2) (get_global_id(0), get_global_id(1));"
    "  uint4 pixel = read_imageui(imageIn, srcSampler, imageCoord);"
    "  write_imageui (imageOut, imageCoord, pixel);"
    "}";


int main( int argc, char *argv[] ) {
    // OpenCL related declarations
    cl_int err;
    cl_platform_id platform;
    cl_device_id device;
    cl_context_properties props[3] = { CL_CONTEXT_PLATFORM, 0, 0 };
    cl_context ctx;
    cl_program program;
    cl_command_queue queue;
    cl_kernel kernel;
    int i;
    int width = 0, height = 0, channel = 0;
    unsigned char *data = NULL;
    const char *filename = NULL;

    if (argc < 2) {
        printf("Please give a filename.\n");
        return 0;
    } else if (argc == 2) {
        filename =  argv[1];
    }

    // Load image data
    data = stbi_load(filename, &width, &height, &channel, 0);
    if(!data) {
        fprintf(stderr, "Open image failed.\n");
        return 0;
    }

    cl_mem myClImageInBuffer;
    cl_mem myClImageOutBuffer;
    cl_sampler sampler;

    cl_image_format format;
    if (channel==4) {
        format.image_channel_order = CL_RGBA;
    } else {
        printf("Not supported image format.\n");
        return 0;
    }
    format.image_channel_data_type = CL_UNSIGNED_INT8;

    err = clGetPlatformIDs( 1, &platform, NULL );
    err = clGetDeviceIDs( platform, CL_DEVICE_TYPE_DEFAULT, 1, &device, NULL );

    cl_bool imageSupport = CL_FALSE;
    clGetDeviceInfo(device, CL_DEVICE_IMAGE_SUPPORT, sizeof(cl_bool),
                    &imageSupport, NULL);

    if (imageSupport != CL_TRUE)
    {
        printf("OpenCL device does not support images.\n");
        return 1;
    }

    props[1] = (cl_context_properties)platform;
    ctx = clCreateContext( props, 1, &device, NULL, NULL, &err );
    queue = clCreateCommandQueueWithProperties( ctx, device, 0, &err );
    program = clCreateProgramWithSource(ctx, 1, (const char **) &kernel_code, NULL, &err);
    err = clBuildProgram(program, 0, NULL, NULL, NULL, NULL);
    kernel = clCreateKernel(program, "PixelAccess", &err);

    //
    // For OpenCL 1.2
    cl_image_desc clImageDesc;
    clImageDesc.image_type = CL_MEM_OBJECT_IMAGE2D;
    clImageDesc.image_width = width;
    clImageDesc.image_height = height;
    clImageDesc.image_row_pitch = 0;
    clImageDesc.image_slice_pitch = 0;
    clImageDesc.num_mip_levels = 0;
    clImageDesc.num_samples = 0;
    clImageDesc.buffer = NULL;

    myClImageInBuffer = clCreateImage(ctx, CL_MEM_READ_ONLY,
                            &format, &clImageDesc, NULL, &err);
    if (!myClImageInBuffer) {
        printf("Create myClImageInBuffer failed.\n");
    }

    myClImageOutBuffer = clCreateImage(ctx, CL_MEM_READ_WRITE,
                            &format, &clImageDesc, NULL, &err);
    if (!myClImageOutBuffer) {
        printf("Create myClImageOutBuffer failed.\n");
    }

    size_t origin[3] = {0, 0, 0};
    size_t region[3] = {width, height, 1};

    err = clEnqueueWriteImage(
            queue, myClImageInBuffer,
            CL_TRUE, origin, region,
            0,
            0, data,
            0, NULL, NULL);

    err = clSetKernelArg(kernel, 0, sizeof(cl_mem), (void *) &myClImageInBuffer);
    err = clSetKernelArg(kernel, 1, sizeof(cl_mem), (void *) &myClImageOutBuffer);

    size_t global_item_size[2] = {width, height};
    size_t local_item_size[2] = {1, 1};

    err = clEnqueueNDRangeKernel(queue, kernel, 2, NULL,
            global_item_size, local_item_size, 0, NULL, NULL);

    err = clFinish(queue);

    unsigned char *data2 = NULL;
    data2 = (unsigned char *) malloc(width * height  * channel);
    err = clEnqueueReadImage( queue,
          myClImageOutBuffer, CL_TRUE,
          origin, region,
          width * sizeof(unsigned char) * 4,
          0, data2,
          0, NULL, NULL);

    err = clFinish(queue);

    stbi_write_png("output.png", width, height, channel, data2, 0);

    free(data2);
    stbi_image_free(data);

    clReleaseMemObject( myClImageInBuffer );
    clReleaseMemObject( myClImageOutBuffer );
    clReleaseKernel( kernel );
    clReleaseProgram( program );
    clReleaseCommandQueue( queue );
    clReleaseContext( ctx );

    return 0;
}

參考連結