diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..5ff6833 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + CompilationDatabase: "build" diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/ci.yaml similarity index 84% rename from .github/workflows/pull_request.yaml rename to .github/workflows/ci.yaml index 066674b..ea97eac 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/ci.yaml @@ -1,13 +1,10 @@ -name: Pull request +name: CI on: pull_request: push: branches: - develop - push: - branches: - - main env: COVERAGE_REPORT_DIR: build/coverage @@ -18,6 +15,9 @@ jobs: build: name: Build library runs-on: ubuntu-latest + strategy: + matrix: + cc: [gcc, clang] container: image: andrelcmoreira/pool-day:v2 volumes: @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v2 - name: Build library run: | - cmake -DCMAKE_BUILD_TYPE=Debug -S . -B build + cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=${{ matrix.cc }} -S . -B build cmake --build build build_samples: @@ -71,6 +71,22 @@ jobs: - name: Run unit tests run: ${{ env.LD_PRELOAD }} build/test/pool-day-tests + static_analysis: + name: Run static analysis + runs-on: ubuntu-latest + container: + image: andrelcmoreira/pool-day:v2 + volumes: + - ${{ github.workspace }}:/pool-day + credentials: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_PASS }} + steps: + - uses: actions/checkout@v2 + + - name: Run cppcheck + run: cppcheck --enable=all --inline-suppr -Iinclude --error-exitcode=1 --suppress=missingIncludeSystem src/* + dynamic_analysis: name: Run dynamic analysis runs-on: ubuntu-latest @@ -139,7 +155,7 @@ jobs: - name: Upload coverage report if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-report path: ${{ env.COVERAGE_REPORT_DIR }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 902ecd7..5d890a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(ENABLE_LOGGING "enable library logs" OFF) set(PROJECT_NAME "pool-day") set(SOURCES src/task.c src/queue.c src/pool_day.c) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wextra -Wpedantic") set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} -g3") set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} -O3 -s") @@ -40,7 +41,7 @@ if(BUILD_DOCUMENTATION) endif(BUILD_DOCUMENTATION) if(ENABLE_LOGGING) - add_definitions(-DLIB_LOGGING) + add_definitions(-DENABLE_LOGGING) list(APPEND SOURCES "src/logger.c") endif(ENABLE_LOGGING) @@ -49,5 +50,5 @@ target_link_libraries(${PROJECT_NAME} pthread) install(TARGETS ${PROJECT_NAME} DESTINATION /usr/lib) install(FILES include/pool_day.h DESTINATION /usr/include/pool_day) +install(FILES include/task.h DESTINATION /usr/include/pool_day) install(FILES include/internal/errors.h DESTINATION /usr/include/pool_day/internal) -install(FILES include/internal/task.h DESTINATION /usr/include/pool_day/internal) diff --git a/README.md b/README.md index 4194e77..98f0d0d 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,30 @@ ## pool-day +[![CI](https://github.com/andrelcmoreira/pool-day/actions/workflows/ci.yaml/badge.svg)](https://github.com/andrelcmoreira/pool-day/actions/workflows/ci.yaml) +[![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) + #### Overview -`pool-day` is a thread pool library for C/C++ applications. +`pool-day` is a simple thread pool library for C/C++ applications built around POSIX threads. + +#### Building -#### Build +The library relies on `cmake` tool to be built: ```cmake $ cmake -S . -B build $ cmake --build build +$ sudo cmake --install build ``` -Optional build flags: +Additional flags can be supplied as parameter to cmake according to the table +below: -| Flag | Description | -|---------------------|----------------------------------------| -| BUILD_SAMPLES | Build the library samples | -| ENABLE_LOGGING | Enable library logs | +| Flag | Description | +|----------------|--------------------------------------| +| BUILD_SAMPLES | Build the library's samples | +| ENABLE_LOGGING | Enable the library's logging feature | -#### Library support +#### Support - linux diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index a4140cc..7b945f1 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -121,7 +121,8 @@ WARN_LOGFILE = #--------------------------------------------------------------------------- # Configuration options related to the input files #--------------------------------------------------------------------------- -INPUT = @CMAKE_CURRENT_SOURCE_DIR@/include @CMAKE_CURRENT_SOURCE_DIR@/src +INPUT = @CMAKE_CURRENT_SOURCE_DIR@/include @CMAKE_CURRENT_SOURCE_DIR@/src @CMAKE_CURRENT_SOURCE_DIR@/README.md +USE_MDFILE_AS_MAINPAGE = README.md INPUT_ENCODING = UTF-8 INPUT_FILE_ENCODING = FILE_PATTERNS = *.c \ diff --git a/include/internal/errors.h b/include/internal/errors.h index 1f48c5c..c482919 100644 --- a/include/internal/errors.h +++ b/include/internal/errors.h @@ -4,7 +4,7 @@ * @brief Contains the definition of the error codes of the library. * * @authors - * Copyright (C) 2023 André L. C. Moreira + * Copyright (C) 2023 André L. C. Moreira * * @copyright * This program is free software: you can redistribute it and/or modify diff --git a/include/internal/logger.h b/include/internal/logger.h index cf7c69e..975e8c5 100644 --- a/include/internal/logger.h +++ b/include/internal/logger.h @@ -4,7 +4,7 @@ * @brief Contains the logging functions of the library. * * @authors - * Copyright (C) 2023 André L. C. Moreira + * Copyright (C) 2023 André L. C. Moreira * * @copyright * This program is free software: you can redistribute it and/or modify @@ -27,19 +27,19 @@ * @brief Log severity. */ typedef enum { - POOL_DAY_INFO_INFO, - POOL_DAY_INFO_ERROR + POOL_DAY_DEBUG_SEVERITY, + POOL_DAY_ERROR_SEVERITY } pool_day_log_severity_t; -#ifdef LIB_LOGGING -#define POOL_DAY_INFO(...) \ - __log_msg(POOL_DAY_INFO_INFO, __FILE__, __func__, __LINE__, __VA_ARGS__) +#ifdef ENABLE_LOGGING +#define POOL_DAY_DEBUG(...) \ + __log_msg(POOL_DAY_DEBUG_SEVERITY, __FILE__, __func__, __LINE__, __VA_ARGS__) #define POOL_DAY_ERROR(...) \ - __log_msg(POOL_DAY_INFO_ERROR, __FILE__, __func__, __LINE__, __VA_ARGS__) + __log_msg(POOL_DAY_ERROR_SEVERITY, __FILE__, __func__, __LINE__, __VA_ARGS__) #else -#define POOL_DAY_INFO(...) +#define POOL_DAY_DEBUG(...) #define POOL_DAY_ERROR(...) -#endif // LIB_LOGGING +#endif // ENABLE_LOGGING /** * @brief Log a message to the screen. diff --git a/include/internal/queue.h b/include/internal/queue.h index ea29bd4..94ed8ef 100644 --- a/include/internal/queue.h +++ b/include/internal/queue.h @@ -4,7 +4,7 @@ * @brief Contains the basic definitions and data structures of the task queue. * * @authors - * Copyright (C) 2023 André L. C. Moreira + * Copyright (C) 2023 André L. C. Moreira * * @copyright * This program is free software: you can redistribute it and/or modify @@ -26,10 +26,10 @@ #include #include -typedef struct task task_t; //!< Structure representing an item on the task - // queue. typedef struct task_queue task_queue_t; //!< Structure representing the queue // itself. +typedef struct task *task_t; //!< Structure representing an item on the task + // queue. /** * @brief Get the queue size. @@ -48,9 +48,9 @@ uint32_t queue_size(task_queue_t *queue); * @note This function is thread-safe. * * @param[in,out] queue Pointer to the queue. - * @param[in] task Pointer to the task to be enqueued. + * @param[in] task Handle to the task to be enqueued. */ -void enqueue(task_queue_t *queue, task_t *task); +void enqueue(task_queue_t *queue, task_t task); /** * @brief Dequeue a task from the queue. @@ -61,7 +61,7 @@ void enqueue(task_queue_t *queue, task_t *task); * * @return The head task. */ -task_t *dequeue(task_queue_t *queue); +task_t dequeue(task_queue_t *queue); /** * @brief Initialize the queue. diff --git a/include/internal/task.h b/include/internal/task.h deleted file mode 100644 index 93e72df..0000000 --- a/include/internal/task.h +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @file - * - * @brief Contains the definitions of task structure and helper methods and - * macros to handle it. - * - * @authors - * Copyright (C) 2023 André L. C. Moreira - * - * @copyright - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ -#ifndef TASK_H_ -#define TASK_H_ - -#include - -/** - * @brief For-each macro implementation. - * - * @details It can be used to iterate over the list. - */ -#define for_each_task(curr, list) \ - for (struct task *curr = list->head; curr; curr = curr->prev) - -/** - * @brief Safe implementation of for-each macro. - * - * @details It can be used to delete elements of the list while iterating over - * it. - */ -#define for_each_task_safe(curr, list) \ - for (struct task *curr = list->head, *tmp = curr ? curr->prev : NULL; \ - curr; \ - curr = tmp, tmp = tmp ? tmp->prev : NULL) - -/** - * @brief Pool task definition. - */ -struct task { - struct task *next; //!< Next element of the current instance. - struct task *prev; //!< Previous element of the current instance. - void *(*task)(void *); //!< Task callback. - void *param; //!< Parameter of the task callback. - void *ret_val; //!< Task return value. - sem_t ready; //!< Task's semaphore. -}; - -typedef struct task task_t; //!< Structure representing an item on the task - // list. -/** - * @brief Create a new task. - * - * @note The created task doesn't require a manual release once it's bound to a - * pool. Otherwise it can be released using the function destroy_task. - * - * @param[in] task Task callback. - * @param[in] param Task parameter. - * - * @return Pointer to the new task. - */ -task_t *create_task(void *(*task)(void *), void *param); - -#endif // TASK_H_ diff --git a/include/internal/task_def.h b/include/internal/task_def.h new file mode 100644 index 0000000..8cb50be --- /dev/null +++ b/include/internal/task_def.h @@ -0,0 +1,58 @@ +/** + * @file + * + * @brief Contains the definitions of task structure and helper methods and + * macros to handle it. + * + * @authors + * Copyright (C) 2023 André L. C. Moreira + * + * @copyright + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +#ifndef TASK_DEF_H_ +#define TASK_DEF_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * @brief Pool task definition. + */ +struct task { + uint32_t id; //!< Task identifier. + bool is_orphan; //!< Flag indicating whether the task is orphaned. + bool auto_release; //!< Flag indicating whether the task must be released after its execution. + struct task *next; //!< Next element of the current instance. + struct task *prev; //!< Previous element of the current instance. + void *(*task)(void *); //!< Task callback. + void *param; //!< Parameter of the task callback. + void *ret_val; //!< Task return value. + void (*on_task_start)(uint32_t, const void *); //!< Callback executed when the task starts. + void (*on_task_end)(uint32_t, const void *, void *); //!< Callback executed when the task ends. + sem_t ready; //!< Task's semaphore. +}; + +typedef struct task *task_t; //!< Structure representing an item on the task queue. + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // TASK_DEF_H_ diff --git a/include/internal/utils.h b/include/internal/utils.h index 146eb1d..ed3e432 100644 --- a/include/internal/utils.h +++ b/include/internal/utils.h @@ -4,7 +4,7 @@ * @brief Contains a set of utilities available for use by the library. * * @authors - * Copyright (C) 2023 André L. C. Moreira + * Copyright (C) 2023 André L. C. Moreira * * @copyright * This program is free software: you can redistribute it and/or modify diff --git a/include/pool_day.h b/include/pool_day.h index 8763022..89f2932 100644 --- a/include/pool_day.h +++ b/include/pool_day.h @@ -4,7 +4,7 @@ * @brief Contains the basic definitions and data structures of the library. * * @authors - * Copyright (C) 2023 André L. C. Moreira + * Copyright (C) 2023 André L. C. Moreira * * @copyright * This program is free software: you can redistribute it and/or modify @@ -31,9 +31,9 @@ extern "C" { #endif // __cplusplus #include "internal/errors.h" -#include "internal/task.h" typedef struct pool_day *pool_day_t; //!< Handle to the pool. +typedef struct task *task_t; //!< Handle to the task. /** * @brief Enqueue a new task into the pool. @@ -43,7 +43,7 @@ typedef struct pool_day *pool_day_t; //!< Handle to the pool. * * @return POOL_DAY_SUCCESS on success; otherwise the suitable error code. */ -pool_day_retcode_t enqueue_task(pool_day_t pool, task_t *task); +pool_day_retcode_t enqueue_task(pool_day_t pool, task_t task); /** * @brief Create a new pool according to the specified size. @@ -81,24 +81,54 @@ uint32_t queued_tasks(pool_day_t pool); * * pool = create_pool(size); * if (pool) { - * task_t *t = create_task(callback, (void *)param); + * task_t t = create_sync_task(id, callback, (void *)param, param_size); * * enqueue_task(pool, t); * - * void *ret = wait_task_finish(pool, t); + * void *ret = get_task_result(t); + * + * handle_ret(ret); + * + * destroy_task(t); * destroy_pool(&pool); * } * @endcode * - * @note This function blocks the current thread. - * @note The task is released after the execution of this function. + * @note This function blocks the current thread and its usage is not safe for + * tasks with 'auto_release' option set. * - * @param[in] pool Handle to the thread pool. - * @param[in] task Task handler. + * @param[in] task Task handle. * * @return The return value of the task. */ -void *wait_task_finish(pool_day_t pool, task_t *task); +void *get_task_result(task_t task); + +/** + * @brief Wait for the finish of a given task without retrieving its result. + * + * Sample: + * @code{.c} + * pool_day_t pool; + * + * pool = create_pool(size); + * if (pool) { + * task_t t = create_sync_task(id, callback, (void *)param, param_size); + * + * enqueue_task(pool, t); + * + * wait_task_finish(t); + * + * destroy_task(t); + * destroy_pool(&pool); + * } + * @endcode + * + * @note This function blocks the current thread and its usage is not safe for + * tasks with 'auto_release' option set. + * + * @param[in] task Task handle. + */ +void wait_task_finish(task_t task); /** * @brief Abort the execution of incoming tasks. diff --git a/include/task.h b/include/task.h new file mode 100644 index 0000000..7d56ffa --- /dev/null +++ b/include/task.h @@ -0,0 +1,86 @@ +/** + * @file + * + * @brief Contains the definitions of task structure and helper methods and + * macros to handle it. + * + * @authors + * Copyright (C) 2023 André L. C. Moreira + * + * @copyright + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +#ifndef TASK_H_ +#define TASK_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +typedef struct task *task_t; //!< Handle to the task. + +/** + * @brief Create a new synchronous task. + * + * @note The created task requires a manual release once it's bound to a + * pool. + * + * @param[in] id Task identifier. + * @param[in] task Task callback. + * @param[in] param Task parameter. + * @param[in] param_size Size of the task parameter. + * + * @return Handle to the new task. + */ +task_t create_sync_task(uint32_t id, void *(*task)(void *), void *param, + size_t param_size); + +/** + * @brief Create a new asynchronous task. + * + * @note When the 'auto_release' parameter is set to false, the created task + * requires a manual release once it's bound to a pool. + * + * @param[in] id Task identifier. + * @param[in] task Task callback. + * @param[in] param Task parameter. + * @param[in] param_size Size of the task parameter. + * @param[in] auto_release Flag indicating whether the task must be released + * after its execution. + * @param[in] start_cb Callback executed when the task starts. + * @param[in] end_cb Callback executed when the task ends. + * + * @return Handle to the new task. + */ +task_t create_async_task(uint32_t id, void *(*task)(void *), void *param, + size_t param_size, bool auto_release, + void (*start_cb)(uint32_t, const void *), + void (*end_cb)(uint32_t, const void *, void *)); + +/** + * @brief Destroy a task. + * + * @param[in] task Handle to the task to be destroyed. + */ +void destroy_task(task_t task); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // TASK_H_ diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 26e351e..7cb4e0b 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -1,4 +1,5 @@ cmake_minimum_required(VERSION 3.18) -add_subdirectory(c) -add_subdirectory(cpp) +add_subdirectory(hello-world-async) +add_subdirectory(hello-world-sync) +add_subdirectory(http-server) diff --git a/samples/cpp/main.cc b/samples/cpp/main.cc deleted file mode 100644 index a6bfc0c..0000000 --- a/samples/cpp/main.cc +++ /dev/null @@ -1,61 +0,0 @@ -#include -#include - -#include "pool_day.h" - -class Foo { - public: - Foo() - : pool_{create_pool(2)}, - t1_{create_task(Foo::Cb1, (void *)"hello")}, - t2_{create_task(Foo::Cb2, (void *)"hi")} { - } - - void RunTasks(void) { - enqueue_task(pool_, t1_); - enqueue_task(pool_, t2_); - - wait_task_finish(pool_, t1_); - wait_task_finish(pool_, t2_); - } - - ~Foo() { - destroy_pool(&pool_); - } - - private: - static void *Cb1(void *param) { - using namespace std::chrono_literals; - - char *msg = reinterpret_cast(param); - for (int i = 0; i < 10; i++) { - std::cout << msg << std::endl; - std::this_thread::sleep_for(2000ms); - } - - return nullptr; - } - - static void *Cb2(void *param) { - using namespace std::chrono_literals; - - char *msg = reinterpret_cast(param); - for (int i = 0; i < 10; i++) { - std::cout << msg << std::endl; - std::this_thread::sleep_for(1000ms); - } - - return nullptr; - } - - pool_day_t pool_; - task_t *t1_; - task_t *t2_; -}; - -int main(void) { - Foo d; - - d.RunTasks(); - return 0; -} diff --git a/samples/c/CMakeLists.txt b/samples/hello-world-async/CMakeLists.txt similarity index 83% rename from samples/c/CMakeLists.txt rename to samples/hello-world-async/CMakeLists.txt index 089a49d..bf559d6 100644 --- a/samples/c/CMakeLists.txt +++ b/samples/hello-world-async/CMakeLists.txt @@ -1,8 +1,8 @@ cmake_minimum_required(VERSION 3.18) -project(pool-day-sample-c) +project(hello-world-async) -set(PROJECT_NAME "pool-day-sample-c") +set(PROJECT_NAME "hello-world-async") set(SOURCES main.c) set(CMAKE_C_FLAGS "-Wall -Werror -Wextra") diff --git a/samples/hello-world-async/README.md b/samples/hello-world-async/README.md new file mode 100644 index 0000000..60a0853 --- /dev/null +++ b/samples/hello-world-async/README.md @@ -0,0 +1,3 @@ +## hello-world-async + +This is a simple asynchronous "Hello, World!" program written using the library. diff --git a/samples/hello-world-async/main.c b/samples/hello-world-async/main.c new file mode 100644 index 0000000..f294b2a --- /dev/null +++ b/samples/hello-world-async/main.c @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + +#include "pool_day.h" +#include "task.h" + +#define HELLO_WORLD_TASK_ID 0 + +void task_start_callback(uint32_t tid, const void *param) { + (void)param; + printf("task '%u' starting...\n", tid); +} + +void task_end_callback(uint32_t tid, const void *param, void *ret_val) { + (void)param; + + printf("task '%u' ended with return value: %s\n", tid, (char *)ret_val); + free(ret_val); +} + +void *func(void *param) { + char *str = (char *)param; + char *ret = (char *)calloc(10, sizeof(char)); + + for (int i = 0; i < 10; i++) { + printf("%s %d\n", str, i); + usleep(1000000); + } + + strcat(ret, "success!"); + + return ret; +} + +int main(void) { + pool_day_t pool; + + pool = create_pool(1); + if (!pool) { + // handle error + exit(EXIT_FAILURE); + } + + char str[] = "hello, world!"; + task_t task = create_async_task(HELLO_WORLD_TASK_ID, func, (void *)str, + sizeof(char) * strlen(str) + 1, false, + task_start_callback, task_end_callback); + + if (!task) { + // handle error + exit(EXIT_FAILURE); + } + + assert(enqueue_task(pool, task) == POOL_DAY_SUCCESS); + + wait_task_finish(task); + + destroy_task(task); + destroy_pool(&pool); + + exit(EXIT_SUCCESS); +} diff --git a/samples/cpp/CMakeLists.txt b/samples/hello-world-sync/CMakeLists.txt similarity index 65% rename from samples/cpp/CMakeLists.txt rename to samples/hello-world-sync/CMakeLists.txt index a7c40b5..79dd6e5 100644 --- a/samples/cpp/CMakeLists.txt +++ b/samples/hello-world-sync/CMakeLists.txt @@ -1,11 +1,11 @@ cmake_minimum_required(VERSION 3.18) -project(pool-day-sample-cpp) +project(hello-world-sync) -set(PROJECT_NAME "pool-day-sample-cpp") -set(SOURCES main.cc) +set(PROJECT_NAME "hello-world-sync") +set(SOURCES main.c) -set(CMAKE_CXX_FLAGS "-Wall -Werror -Wextra") +set(CMAKE_C_FLAGS "-Wall -Werror -Wextra") include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../../include diff --git a/samples/hello-world-sync/README.md b/samples/hello-world-sync/README.md new file mode 100644 index 0000000..1688005 --- /dev/null +++ b/samples/hello-world-sync/README.md @@ -0,0 +1,3 @@ +## hello-world-sync + +This is a simple synchronous "Hello, World!" program written using the library. diff --git a/samples/c/main.c b/samples/hello-world-sync/main.c similarity index 64% rename from samples/c/main.c rename to samples/hello-world-sync/main.c index 54435dd..d69b479 100644 --- a/samples/c/main.c +++ b/samples/hello-world-sync/main.c @@ -5,6 +5,9 @@ #include #include "pool_day.h" +#include "task.h" + +#define HELLO_WORLD_TASK_ID 0 void *func(void *param) { char *str = (char *)param; @@ -22,7 +25,6 @@ void *func(void *param) { int main(void) { pool_day_t pool; - task_t *task; pool = create_pool(1); if (!pool) { @@ -30,14 +32,22 @@ int main(void) { exit(EXIT_FAILURE); } - task = create_task(func, (void *)"foo"); + char str[] = "hello, world!"; + task_t task = create_sync_task(HELLO_WORLD_TASK_ID, func, (void *)str, + sizeof(char) * strlen(str) + 1); - assert(enqueue_task(pool, task) == POOL_DAY_SUCCESS); - char *ret = (char *)wait_task_finish(pool, task); + if (!task) { + // handle error + exit(EXIT_FAILURE); + } - destroy_pool(&pool); + assert(enqueue_task(pool, task) == POOL_DAY_SUCCESS); + char *ret = (char *)get_task_result(task); printf("result = %s\n", ret); + + destroy_task(task); + destroy_pool(&pool); free(ret); exit(EXIT_SUCCESS); diff --git a/samples/http-server/CMakeLists.txt b/samples/http-server/CMakeLists.txt new file mode 100644 index 0000000..128233b --- /dev/null +++ b/samples/http-server/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.18) + +project(http-server) + +set(PROJECT_NAME "http-server") +set(SOURCES main.c) + +set(CMAKE_C_FLAGS "-Wall -Werror -Wextra") + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) +link_directories(${CMAKE_CURRENT_LIST_DIR}/../../../build) + +add_executable(${PROJECT_NAME} ${SOURCES}) +target_link_libraries(${PROJECT_NAME} pool-day) diff --git a/samples/http-server/README.md b/samples/http-server/README.md new file mode 100644 index 0000000..52fd953 --- /dev/null +++ b/samples/http-server/README.md @@ -0,0 +1,7 @@ +## http-server + +This is an extremely simple static http server built with the library. Supported command line options: + +- `-m`: Maximum number of concurrent clients (default: 100) +- `-p`: Port number to listen on (default: 8080) +- `-r`: Root directory to serve files from (default: ./www) diff --git a/samples/http-server/main.c b/samples/http-server/main.c new file mode 100644 index 0000000..1b29d0f --- /dev/null +++ b/samples/http-server/main.c @@ -0,0 +1,430 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pool_day.h" +#include "task.h" + +#define MAX_METHOD_SIZE 7 +#define MAX_BUFFER_SIZE 10 * 1024 + +#define DEFAULT_ROOT_DIR "./www" +#define DEFAULT_PORT 8080 +#define DEFAULT_MAX_CLIENTS 100 + +#define CONTENT_TYPE_TEXT_HTML "text/html" +#define CONTENT_TYPE_IMAGE_PNG "image/png" +#define CONTENT_TYPE_IMAGE_JPEG "image/jpeg" +#define CONTENT_TYPE_IMAGE_GIF "image/gif" +#define CONTENT_TYPE_TEXT_CSS "text/css" +#define CONTENT_TYPE_APP_JS "application/javascript" +#define CONTENT_TYPE_APP_OCTET "application/octet-stream" + +#define MAKE_ERROR_BODY(code, msg) \ + "

" #code " " #msg "

" + +typedef struct { + uint32_t max_clients; + uint16_t port; + char root_dir[PATH_MAX]; +} server_cfg_t; + +typedef struct { + char method[MAX_METHOD_SIZE]; + char resource[PATH_MAX]; +} request_t; + +typedef struct { + uint32_t fd; + struct in_addr addr; +} client_t; + +static void sig_handler(int signum) { + (void)signum; +} + +static void on_client_connected(uint32_t tid, const void *param) { + const client_t *cli = (client_t *)param; + + fprintf(stdout, "[+] task[%u]: client connected, ip=%s\n", tid, + inet_ntoa(cli->addr)); +} + +static void on_client_disconnected(uint32_t tid, const void *param, + void *ret_val) { + const client_t *cli = (client_t *)param; + int ret = ret_val ? *((uint16_t *)ret_val) : -1; + + fprintf(stdout, "[+] task[%u]: client disconnected ip=%s, result=%u\n", tid, + inet_ntoa(cli->addr), ret); + + if (ret_val) { + free(ret_val); + } +} + +static const char *get_content_type(const char *path) { + const char *ext = strrchr(path, '.'); + + if (!ext) { + return CONTENT_TYPE_APP_OCTET; + } + + if (!strcmp(ext, ".html") || !strcmp(ext, ".htm")) { + return CONTENT_TYPE_TEXT_HTML; + } + + if (!strcmp(ext, ".png")) { + return CONTENT_TYPE_IMAGE_PNG; + } + + if (!strcmp(ext, ".jpg") || !strcmp(ext, ".jpeg")) { + return CONTENT_TYPE_IMAGE_JPEG; + } + + if (!strcmp(ext, ".gif")) { + return CONTENT_TYPE_IMAGE_GIF; + } + + if (!strcmp(ext, ".css")) { + return CONTENT_TYPE_TEXT_CSS; + } + + if (!strcmp(ext, ".js")) { + return CONTENT_TYPE_APP_JS; + } + + return CONTENT_TYPE_APP_OCTET; +} + +static void parse_request(const char *buffer, request_t *req) { + sscanf(buffer, "%s %s", req->method, req->resource); +} + +static char *build_http_header(int status_code, const char *status_str, + const char *content_type, size_t content_length, + size_t *header_len) { + + const char *fmt = + "HTTP/1.1 %d %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n"; + + *header_len = snprintf(NULL, 0, fmt, status_code, status_str, content_type, + content_length); + + char *header = malloc(*header_len + 1); + if (!header) { + return NULL; + } + + snprintf(header, *header_len + 1, fmt, status_code, status_str, content_type, + content_length); + + return header; +} + +static int assemble_reply(char **buffer, const char *header, size_t header_len, + const char *body, size_t body_len) { + size_t total = header_len + body_len; + + *buffer = malloc(total); + if (!(*buffer)) { + return 0; + } + + memcpy(*buffer, header, header_len); + memcpy(*buffer + header_len, body, body_len); + + return total; +} + +static char *get_resource(const char *path, size_t *len) { + struct stat st; + + FILE *file = fopen(path, "rb"); + if (!file) { + return NULL; + } + + if (stat(path, &st) < 0) { + fclose(file); + return NULL; + } + + char *data = malloc(st.st_size); + if (!data) { + fclose(file); + return NULL; + } + + size_t read_bytes = fread(data, 1, st.st_size, file); + fclose(file); + + if (read_bytes != (size_t)st.st_size) { + free(data); + return NULL; + } + + *len = read_bytes; + return data; +} + +static int handle_get_request(char **reply_buffer, size_t *reply_buffer_size, + const char *resource) { + size_t header_len; + + if (strstr(resource, "..")) { + return 400; + } + + size_t res_len; + char *res = get_resource(resource, &res_len); + + if (res) { + const char *content_type = get_content_type(resource); + char *header = build_http_header(200, "OK", content_type, res_len, + &header_len); + + *reply_buffer_size = assemble_reply(reply_buffer, header, header_len, res, + res_len); + + free(header); + free(res); + + return 200; + } + + const char *body = MAKE_ERROR_BODY(404, Not Found); + size_t body_len = strlen(body); + char *header = build_http_header(404, "Not Found", CONTENT_TYPE_TEXT_HTML, + body_len, &header_len); + + *reply_buffer_size = assemble_reply(reply_buffer, header, header_len, body, + body_len); + + free(header); + + return 404; +} + +static int handle_request(const char *req_buffer, + char **reply_buffer, + size_t *reply_buffer_len) { + + request_t req; + + memset(&req, 0, sizeof(request_t)); + + parse_request(req_buffer, &req); + if (strcmp(req.method, "GET") != 0) { + return 501; + } + + fprintf(stdout, "[+] %s %s\n", req.method, req.resource); + + const char *resource = req.resource[0] == '/' ? + req.resource + 1 : + req.resource; + + return handle_get_request(reply_buffer, reply_buffer_len, resource); +} + +static void send_all(int fd, const char *buffer, size_t len) { + size_t total = 0; + + while (total < len) { + ssize_t sent = send(fd, buffer + total, len - total, 0); + + if (sent <= 0) { + break; + } + + total += sent; + } +} + +static void *handle_new_connection(void *param) { + client_t *cli = (client_t *)param; + int *ret = NULL; + + char *req_buffer = calloc(1, MAX_BUFFER_SIZE); + if (!req_buffer) { + close(cli->fd); + return NULL; + } + + ssize_t received = recv(cli->fd, req_buffer, MAX_BUFFER_SIZE - 1, 0); + + if (received > 0) { + char *reply_buffer = NULL; + size_t reply_len = 0; + + ret = calloc(1, sizeof(int)); + *ret = handle_request(req_buffer, &reply_buffer, &reply_len); + + if (reply_buffer && reply_len > 0) { + send_all(cli->fd, reply_buffer, reply_len); + free(reply_buffer); + } + } + + free(req_buffer); + close(cli->fd); + + return (void *)ret; +} + +static void fill_cfg(int argc, char **argv, server_cfg_t *cfg) { + int opt; + + while ((opt = getopt(argc, argv, "m:p:r:")) != -1) { + switch (opt) { + case 'm': + cfg->max_clients = atoi(optarg); + break; + case 'p': + cfg->port = atoi(optarg); + break; + case 'r': + strcpy(cfg->root_dir, optarg); + break; + } + } + + if (!cfg->max_clients) { + cfg->max_clients = DEFAULT_MAX_CLIENTS; + } + + if (!cfg->port) { + cfg->port = DEFAULT_PORT; + } + + if (!cfg->root_dir[0]) { + strcpy(cfg->root_dir, DEFAULT_ROOT_DIR); + } +} + +static int setup_socket(int *sock_fd, const server_cfg_t *cfg) { + struct sockaddr_in addr; + + *sock_fd = socket(AF_INET, SOCK_STREAM, 0); + if (*sock_fd < 0) { + return 1; + } + + memset(&addr, 0, sizeof(addr)); + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(cfg->port); + + if (bind(*sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(*sock_fd); + return 1; + } + + if (listen(*sock_fd, cfg->max_clients) < 0) { + close(*sock_fd); + return 1; + } + + return 0; +} + +static int setup_server(int *sock_fd, const server_cfg_t *cfg, + pool_day_t *pool) { + if (setup_socket(sock_fd, cfg) != 0) { + return 1; + } + + if (!(*pool = create_pool(cfg->max_clients))) { + close(*sock_fd); + return 1; + } + + return 0; +} + +static int server_mainloop(int server_fd, pool_day_t pool) { + fd_set set; + + while (1) { + FD_ZERO(&set); + FD_SET(server_fd, &set); + + if (select(server_fd + 1, &set, NULL, NULL, NULL) < 0) { + break; + } + + if (FD_ISSET(server_fd, &set)) { + struct sockaddr_in cli_addr; + socklen_t len = sizeof(cli_addr); + + int client_fd = accept(server_fd, (struct sockaddr *)&cli_addr, &len); + if (client_fd < 0) { + continue; + } + + task_t task = create_async_task(client_fd, handle_new_connection, + (void *)&((client_t) { + .fd = (uint32_t)client_fd, + .addr = cli_addr.sin_addr }), + sizeof(client_t), true, + on_client_connected, + on_client_disconnected); + + enqueue_task(pool, task); + } + } + + close(server_fd); + destroy_pool(&pool); + + return 0; +} + +static int run_server(const server_cfg_t *cfg) { + int server_fd; + pool_day_t pool; + + if (setup_server(&server_fd, cfg, &pool)) { + fprintf(stderr, "[-] fail to setup the server\n"); + return 1; + } + + if (chdir(cfg->root_dir)) { + fprintf(stderr, "[-] fail to run the server on '%s': %s\n", cfg->root_dir, + strerror(errno)); + return 1; + } + + fprintf(stdout, + "[+] starting server with max_clients=%u, port=%u, root_dir=%s\n", + cfg->max_clients, cfg->port, cfg->root_dir); + + return server_mainloop(server_fd, pool); +} + +int main(int argc, char **argv) { + server_cfg_t cfg; + + signal(SIGINT, sig_handler); + + memset(&cfg, 0, sizeof(server_cfg_t)); + fill_cfg(argc, argv, &cfg); + + return run_server(&cfg); +} diff --git a/src/logger.c b/src/logger.c index b7a9213..576b5c7 100644 --- a/src/logger.c +++ b/src/logger.c @@ -1,32 +1,32 @@ #include "internal/logger.h" +#include #include #include #include -#include #include "internal/utils.h" -#define GET_LOG_FILE(sev) (sev == POOL_DAY_INFO_INFO) ? stdout : stderr -#define GET_LOG_TAG(sev) (sev == POOL_DAY_INFO_INFO) ? "INFO" : "ERROR" +#define GET_LOG_FILE(sev) (sev == POOL_DAY_DEBUG_SEVERITY) ? stdout : stderr +#define GET_LOG_TAG(sev) (sev == POOL_DAY_DEBUG_SEVERITY) ? "DEBUG" : "ERROR" #define GET_FILE_NAME(path) strrchr(path, '/') + 1 -#define LOG_FMT "%s:%d\t| %s\t| %s:" +#define LOG_FMT "%s | %s:%d | %s | %s\n" #define LOG_BUFFER_SIZE 4096 static pthread_mutex_t log_mutex; //!< Mutex for log operations. void __log_msg(pool_day_log_severity_t sev, const char *file_name, const char *func_name, int line_no, const char *fmt, ...) { - char log_buffer[LOG_BUFFER_SIZE] = {0}; - va_list args; - THREAD_SAFE_ZONE(&log_mutex, { + char log_buffer[LOG_BUFFER_SIZE] = {0}; + va_list args; + va_start(args, fmt); vsnprintf(log_buffer, LOG_BUFFER_SIZE, fmt, args); - fprintf(GET_LOG_FILE(sev), LOG_FMT " %s\n", GET_FILE_NAME(file_name), - line_no, func_name, GET_LOG_TAG(sev), log_buffer); + fprintf(GET_LOG_FILE(sev), LOG_FMT, GET_LOG_TAG(sev), + GET_FILE_NAME(file_name), line_no, func_name, log_buffer); va_end(args); }); diff --git a/src/pool_day.c b/src/pool_day.c index 03e21e6..b547246 100644 --- a/src/pool_day.c +++ b/src/pool_day.c @@ -4,19 +4,19 @@ #include #include #include -#include #include "internal/logger.h" #include "internal/queue.h" +#include "internal/task_def.h" #include "internal/utils.h" +#include "task.h" /** * @brief Main structure of the library, it defines a handle to the pool. */ struct pool_day { uint32_t size; //!< Size of the pool. - bool must_stop; //!< Flag indicating wheter all threads must - // stop its execution. + bool must_stop; //!< Flag indicating wheter all threads must stop its execution. sem_t lock; //!< Pool's semaphore. pthread_t *threads; //!< Threads whose makes part of the pool. task_queue_t *tasks; //!< Pool's queued tasks. @@ -26,49 +26,80 @@ __static void *thread_func(void *param) { pool_day_t pool = (pool_day_t)param; while (!pool->must_stop) { + POOL_DAY_DEBUG("thread '0x%x' waiting...", pthread_self()); sem_wait(&pool->lock); - - POOL_DAY_INFO("thread '0x%x' woke up", pthread_self()); + POOL_DAY_DEBUG("thread '0x%x' woke up", pthread_self()); if (pool->must_stop) { - POOL_DAY_INFO("thread '0x%x' aborting...", pthread_self()); + POOL_DAY_DEBUG("thread '0x%x' aborting...", pthread_self()); break; } - task_t *entry = dequeue(pool->tasks); - if (entry) { - POOL_DAY_INFO("thread '0x%x' running the task", pthread_self()); - void *ret = entry->task(entry->param); - POOL_DAY_INFO("thread '0x%x' finished the task", pthread_self()); + task_t entry = dequeue(pool->tasks); + if (!entry) { + POOL_DAY_DEBUG("thread '0x%x' found no task to execute", pthread_self()); + continue; + } + + entry->is_orphan = true; + + if (entry->on_task_start) { + POOL_DAY_DEBUG("thread '0x%x' executing start callback for task '0x%x'", + pthread_self(), entry->id); + entry->on_task_start(entry->id, entry->param); + } + + POOL_DAY_DEBUG("thread '0x%x' running the task '0x%x'", pthread_self(), + entry->id); + void *ret = entry->task(entry->param); + POOL_DAY_DEBUG("thread '0x%x' finished the task '0x%x'", pthread_self(), + entry->id); + + if (entry->on_task_end) { + POOL_DAY_DEBUG("thread '0x%x' executing end callback for task '0x%x'", + pthread_self(), entry->id); + entry->on_task_end(entry->id, entry->param, ret); + } + if (!entry->auto_release) { entry->ret_val = ret; sem_post(&entry->ready); + } else { + destroy_task(entry); } } - POOL_DAY_INFO("thread '0%x' finishing...", pthread_self()); + POOL_DAY_DEBUG("thread '0%x' finishing...", pthread_self()); return NULL; } -pool_day_retcode_t enqueue_task(pool_day_t pool, task_t *task) { +// cppcheck-suppress unusedFunction +pool_day_retcode_t enqueue_task(pool_day_t pool, task_t task) { if (!pool || !task) { - POOL_DAY_ERROR("null parameter"); + POOL_DAY_ERROR("null parameter supplied"); return POOL_DAY_ERROR_NULL_PARAM; } + POOL_DAY_DEBUG("task id=%u, callback=%p, parameter=%p", task->id, task->task, + task->param); + + task->ret_val = NULL; + task->is_orphan = false; + enqueue(pool->tasks, task); sem_post(&pool->lock); - POOL_DAY_INFO("task enqueued with success"); + POOL_DAY_DEBUG("task enqueued with success"); return POOL_DAY_SUCCESS; } +// cppcheck-suppress unusedFunction pool_day_t create_pool(uint32_t pool_size) { pool_day_t pool; - POOL_DAY_INFO("pool size: %u", pool_size); + POOL_DAY_DEBUG("pool_size=%u", pool_size); if (!pool_size) { POOL_DAY_ERROR("bad pool size"); @@ -96,34 +127,35 @@ pool_day_t create_pool(uint32_t pool_size) { for (uint32_t i = 0; i < pool_size; i++) { pthread_create(&pool->threads[i], NULL, thread_func, pool); - POOL_DAY_INFO("thread '%u' created", i); + POOL_DAY_DEBUG("thread '%u' created", i); } - POOL_DAY_INFO("pool created with success"); + POOL_DAY_DEBUG("pool created with success"); return pool; } +// cppcheck-suppress unusedFunction pool_day_retcode_t destroy_pool(pool_day_t *pool) { if (!pool || !(*pool)) { POOL_DAY_ERROR("null pool handle"); return POOL_DAY_ERROR_NULL_PARAM; } - POOL_DAY_INFO("waking up all sleeping threads"); + POOL_DAY_DEBUG("waking up all sleeping threads"); (*pool)->must_stop = true; for (uint32_t i = 0; i < (*pool)->size; i++) { - POOL_DAY_INFO("waking up thread '%u'", i); + POOL_DAY_DEBUG("waking up thread '%u'", i); sem_post(&(*pool)->lock); } - POOL_DAY_INFO("joining all threads of the pool"); + POOL_DAY_DEBUG("joining all threads of the pool"); for (uint32_t i = 0; i < (*pool)->size; i++) { - POOL_DAY_INFO("finishing thread '%u'", i); + POOL_DAY_DEBUG("finishing thread '%u'", i); pthread_join((*pool)->threads[i], NULL); - POOL_DAY_INFO("thread '%u' finished", i); + POOL_DAY_DEBUG("thread '%u' finished", i); } free((*pool)->threads); @@ -133,11 +165,12 @@ pool_day_retcode_t destroy_pool(pool_day_t *pool) { free(*pool); *pool = NULL; - POOL_DAY_INFO("pool destroyed with success"); + POOL_DAY_DEBUG("pool destroyed with success"); return POOL_DAY_SUCCESS; } +// cppcheck-suppress unusedFunction pool_day_retcode_t abort_tasks(pool_day_t pool) { if (!pool) { POOL_DAY_ERROR("null pool handle"); @@ -146,29 +179,36 @@ pool_day_retcode_t abort_tasks(pool_day_t pool) { pool->must_stop = true; - POOL_DAY_INFO("stopping all threads"); + POOL_DAY_DEBUG("stopping all threads"); return POOL_DAY_SUCCESS; } +// cppcheck-suppress unusedFunction uint32_t queued_tasks(pool_day_t pool) { return pool ? queue_size(pool->tasks) : 0; } -void *wait_task_finish(pool_day_t pool, task_t *task) { - void *ret; +void *get_task_result(task_t task) { + if (!task) { + POOL_DAY_ERROR("null task provided"); + return NULL; + } - if (!pool || !task) { - POOL_DAY_ERROR("null pool handle or task"); + if (task->auto_release) { + POOL_DAY_ERROR( + "the result of a task with 'auto_release' option set can't be retrieved"); return NULL; } - POOL_DAY_INFO("waiting for the finish of the task"); + POOL_DAY_DEBUG("waiting for the finish of the task"); sem_wait(&task->ready); - POOL_DAY_INFO("task finished"); + POOL_DAY_DEBUG("task finished"); - ret = task->ret_val; - free(task); + return task->ret_val; +} - return ret; +// cppcheck-suppress unusedFunction +void wait_task_finish(task_t task) { + (void)get_task_result(task); } diff --git a/src/queue.c b/src/queue.c index 264efe7..7cb3c1b 100644 --- a/src/queue.c +++ b/src/queue.c @@ -2,15 +2,35 @@ #include #include "internal/queue.h" -#include "internal/task.h" +#include "internal/task_def.h" #include "internal/utils.h" +#include "task.h" + +/** + * @brief For-each macro implementation. + * + * @details It can be used to iterate over the queue. + */ +#define for_each_task(curr, queue) \ + for (struct task *curr = queue->head; curr; curr = curr->prev) + +/** + * @brief Safe implementation of for-each macro. + * + * @details It can be used to delete elements of the queue while iterating over + * it. + */ +#define for_each_task_safe(curr, queue) \ + for (struct task *curr = queue->head, *tmp = curr ? curr->prev : NULL; \ + curr; \ + curr = tmp, tmp = tmp ? tmp->prev : NULL) /** * @brief Task queue definition. */ struct task_queue { - task_t *tail; //!< Tail of the queue. - task_t *head; //!< Head of the queue. + task_t tail; //!< Tail of the queue. + task_t head; //!< Head of the queue. pthread_mutex_t mutex; //!< Mutex of the queue. }; @@ -28,7 +48,7 @@ uint32_t queue_size(task_queue_t *queue) { return size; } -void enqueue(task_queue_t *queue, task_t *elem) { +void enqueue(task_queue_t *queue, task_t elem) { if (queue && elem) { THREAD_SAFE_ZONE(&queue->mutex, { // if the queue is empty @@ -43,10 +63,10 @@ void enqueue(task_queue_t *queue, task_t *elem) { } } -static task_t *__dequeue(task_queue_t *queue) { - task_t *to_del = queue->head; +static task_t __dequeue(task_queue_t *queue) { + task_t to_del = queue->head; - // if the list is not empty + // if the queue is not empty if (to_del) { queue->head = to_del->prev; // if the queue has more than one element @@ -61,8 +81,8 @@ static task_t *__dequeue(task_queue_t *queue) { return to_del; } -task_t *dequeue(task_queue_t *queue) { - task_t *to_del = NULL; +task_t dequeue(task_queue_t *queue) { + task_t to_del = NULL; if (queue) { THREAD_SAFE_ZONE(&queue->mutex, { @@ -86,9 +106,9 @@ void destroy_queue(task_queue_t *queue) { if (queue) { THREAD_SAFE_ZONE(&queue->mutex, { for_each_task_safe(curr, queue) { - task_t *node = __dequeue(queue); + task_t node = __dequeue(queue); - free(node); + destroy_task(node); } }) diff --git a/src/task.c b/src/task.c index da24ad2..8dcfa3c 100644 --- a/src/task.c +++ b/src/task.c @@ -1,19 +1,51 @@ -#include "internal/task.h" +#include "task.h" #include +#include -task_t *create_task(void *(*task)(void *), void *param) { - task_t *node; +#include "internal/task_def.h" - node = (task_t *)calloc(1, sizeof(task_t)); +// cppcheck-suppress unusedFunction +task_t create_sync_task(uint32_t id, void *(*task)(void *), void *param, + size_t param_size) { + return create_async_task(id, task, param, param_size, false, NULL, NULL); +} + +task_t create_async_task(uint32_t id, void *(*task)(void *), void *param, + size_t param_size, bool auto_release, + void (*start_cb)(uint32_t, const void *), + void (*end_cb)(uint32_t, const void *, void *)) { + task_t node; + + node = (task_t)calloc(1, sizeof(*node)); if (node) { - node->next = node->prev = NULL; + node->id = id; node->task = task; - node->param = param; - node->ret_val = NULL; + node->on_task_start = start_cb; + node->on_task_end = end_cb; + node->is_orphan = true; + node->auto_release = auto_release; + + if (param) { + node->param = malloc(param_size); + memcpy(node->param, param, param_size); + } sem_init(&node->ready, 0, 0); } return node; } + +void destroy_task(task_t task) { + // if the task is orphaned, it means that the user will take care of its release, + // otherwise its destruction is managed by the pool + if (task && task->is_orphan) { + sem_destroy(&task->ready); + + if (task->param) { + free(task->param); + } + free(task); + } +} diff --git a/test/src/mock/callback_mock.cc b/test/src/mock/callback_mock.cc index 9deed48..ce71b3a 100644 --- a/test/src/mock/callback_mock.cc +++ b/test/src/mock/callback_mock.cc @@ -16,6 +16,14 @@ void *CbWrapper::TaskCb(void *param) { return mock_->TaskCb(param); } +void CbWrapper::OnTaskStartCb(uint32_t tid, const void *param) { + mock_->OnTaskStartCb(tid, param); +} + +void CbWrapper::OnTaskEndCb(uint32_t tid, const void *param, void *ret_val) { + mock_->OnTaskEndCb(tid, param, ret_val); +} + TaskMock &CbWrapper::mock() { return *CbWrapper::mock_; } diff --git a/test/src/mock/callback_mock.h b/test/src/mock/callback_mock.h index 2572191..8190454 100644 --- a/test/src/mock/callback_mock.h +++ b/test/src/mock/callback_mock.h @@ -6,6 +6,8 @@ class TaskMock { public: MOCK_METHOD(void *, TaskCb, (void *)); + MOCK_METHOD(void, OnTaskStartCb, (uint32_t, const void *)); + MOCK_METHOD(void, OnTaskEndCb, (uint32_t, const void *, void *)); }; class CbWrapper { @@ -14,6 +16,8 @@ class CbWrapper { ~CbWrapper(); static void *TaskCb(void *param); + static void OnTaskStartCb(uint32_t tid, const void *param); + static void OnTaskEndCb(uint32_t tid, const void *param, void *ret_val); static TaskMock &mock(); private: diff --git a/test/src/pool_day_test.cc b/test/src/pool_day_test.cc index 2438365..fcc1310 100644 --- a/test/src/pool_day_test.cc +++ b/test/src/pool_day_test.cc @@ -3,7 +3,9 @@ #include "mock/callback_mock.h" extern "C" { +#include "internal/task_def.h" #include "pool_day.h" +#include "task.h" extern void *thread_func(void *param); } @@ -36,6 +38,13 @@ class PoolDayTest : public Test { CbWrapper wrapper_; //!< Callback instance. }; +/** + * @brief Custom matcher to compare void pointers containing strings. + */ +MATCHER_P(StrEqVoidPointer, expected_string, "") { + return std::string(static_cast(arg)) == expected_string; +} + /** * @brief When we try to create a pool with size 0, then null must be returned. */ @@ -48,11 +57,14 @@ TEST_F(PoolDayTest, CreatePollWithInvalidSize) { * it, then it must be added to the pool's task queue. */ TEST_F(PoolDayTest, EnqueueSingleTaskWithPoolEmpty) { - auto task = create_task(nullptr, nullptr); + auto task = create_sync_task(0, nullptr, nullptr, 0); EXPECT_EQ(queued_tasks(pool_), 0); EXPECT_EQ(enqueue_task(pool_, task), POOL_DAY_SUCCESS); EXPECT_EQ(queued_tasks(pool_), 1); + + // to force the task destruction on destroy_pool call + task->is_orphan = true; } /** @@ -60,11 +72,11 @@ TEST_F(PoolDayTest, EnqueueSingleTaskWithPoolEmpty) { * it, then they must be added to the pool's task queue. */ TEST_F(PoolDayTest, EnqueueTaskWithPoolNotEmpty) { - auto t1 = create_task(nullptr, nullptr); - auto t2 = create_task(nullptr, nullptr); - auto t3 = create_task(nullptr, nullptr); - auto t4 = create_task(nullptr, nullptr); - auto t5 = create_task(nullptr, nullptr); + auto t1 = create_sync_task(0, nullptr, nullptr, 0); + auto t2 = create_sync_task(1, nullptr, nullptr, 0); + auto t3 = create_sync_task(2, nullptr, nullptr, 0); + auto t4 = create_sync_task(3, nullptr, nullptr, 0); + auto t5 = create_sync_task(4, nullptr, nullptr, 0); EXPECT_EQ(queued_tasks(pool_), 0); @@ -75,6 +87,13 @@ TEST_F(PoolDayTest, EnqueueTaskWithPoolNotEmpty) { EXPECT_EQ(enqueue_task(pool_, t5), POOL_DAY_SUCCESS); EXPECT_EQ(queued_tasks(pool_), 5); + + // to force the task destruction on destroy_pool call + t1->is_orphan = true; + t2->is_orphan = true; + t3->is_orphan = true; + t4->is_orphan = true; + t5->is_orphan = true; } /** @@ -82,7 +101,7 @@ TEST_F(PoolDayTest, EnqueueTaskWithPoolNotEmpty) { * then nothing must happen and the suitable error code must be returned. */ TEST_F(PoolDayTest, EnqueueTaskWithNullPool) { - auto task = create_task(nullptr, nullptr); + auto task = create_sync_task(0, nullptr, nullptr, 0); EXPECT_EQ(enqueue_task(nullptr, task), POOL_DAY_ERROR_NULL_PARAM); free(task); @@ -93,7 +112,7 @@ TEST_F(PoolDayTest, EnqueueTaskWithNullPool) { * then nothing must happen and the suitable error code must be returned. */ TEST_F(PoolDayTest, EnqueueTaskWithNullTask) { - auto task = create_task(nullptr, nullptr); + auto task = create_sync_task(0, nullptr, nullptr, 0); EXPECT_EQ(enqueue_task(pool_, nullptr), POOL_DAY_ERROR_NULL_PARAM); free(task); @@ -112,13 +131,14 @@ TEST_F(PoolDayTest, GetQueuedTasksCountWithNoTasks) { * the number of queued tasks in the pool, then 1 must be returned. */ TEST_F(PoolDayTest, GetQueuedTasksCountWithSingleTask) { - { - auto task = create_task(nullptr, nullptr); + auto task = create_sync_task(0, nullptr, nullptr, 0); - enqueue_task(pool_, task); - } + enqueue_task(pool_, task); EXPECT_EQ(queued_tasks(pool_), 1); + + // to force the task destruction on destroy_pool call + task->is_orphan = true; } /** @@ -127,21 +147,26 @@ TEST_F(PoolDayTest, GetQueuedTasksCountWithSingleTask) { * returned. */ TEST_F(PoolDayTest, GetQueuedTasksCountWithSeveralTasks) { - { - auto t1 = create_task(nullptr, nullptr); - auto t2 = create_task(nullptr, nullptr); - auto t3 = create_task(nullptr, nullptr); - auto t4 = create_task(nullptr, nullptr); - auto t5 = create_task(nullptr, nullptr); - - enqueue_task(pool_, t1); - enqueue_task(pool_, t2); - enqueue_task(pool_, t3); - enqueue_task(pool_, t4); - enqueue_task(pool_, t5); - } + auto t1 = create_sync_task(0, nullptr, nullptr, 0); + auto t2 = create_sync_task(1, nullptr, nullptr, 0); + auto t3 = create_sync_task(2, nullptr, nullptr, 0); + auto t4 = create_sync_task(3, nullptr, nullptr, 0); + auto t5 = create_sync_task(4, nullptr, nullptr, 0); + + enqueue_task(pool_, t1); + enqueue_task(pool_, t2); + enqueue_task(pool_, t3); + enqueue_task(pool_, t4); + enqueue_task(pool_, t5); EXPECT_EQ(queued_tasks(pool_), 5); + + // to force the task destruction on destroy_pool call + t1->is_orphan = true; + t2->is_orphan = true; + t3->is_orphan = true; + t4->is_orphan = true; + t5->is_orphan = true; } /** @@ -167,11 +192,13 @@ TEST_F(PoolDayTest, DestroyPollWithNullHandle) { * scheduled for execution, then the task's callback must be called. */ TEST_F(PoolDayTest, ExecuteTaskWithNullParameterWithSuccess) { - auto task = create_task(CbWrapper::TaskCb, nullptr); + auto task = create_sync_task(0, CbWrapper::TaskCb, nullptr, 0); char ret_val[]{ "hello, i'm the return of the task" }; enqueue_task(pool_, task); + EXPECT_CALL(CbWrapper::mock(), OnTaskStartCb(_, _)) + .Times(0); EXPECT_CALL(CbWrapper::mock(), TaskCb(nullptr)) .Times(1) .WillOnce( @@ -180,13 +207,141 @@ TEST_F(PoolDayTest, ExecuteTaskWithNullParameterWithSuccess) { return ret_val; }) ); + EXPECT_CALL(CbWrapper::mock(), OnTaskEndCb(_, _, _)) + .Times(0); auto ret = thread_func(pool_); EXPECT_EQ(ret, nullptr); EXPECT_EQ(task->ret_val, ret_val); // cleanup - free(task); + destroy_task(task); +} + +/** + * @brief Given the pool has one task with callbacks, when the task is + * scheduled for execution, then the task's callbacks must be called at the + * suitable times. + */ +TEST_F(PoolDayTest, ExecuteTaskWithCallbacks) { + auto task = create_async_task(0, CbWrapper::TaskCb, nullptr, 0, false, + CbWrapper::OnTaskStartCb, + CbWrapper::OnTaskEndCb); + char ret_val[]{ "hello, i'm the return of the task" }; + + enqueue_task(pool_, task); + + EXPECT_CALL(CbWrapper::mock(), OnTaskStartCb(task->id, task->param)) + .Times(1); + EXPECT_CALL(CbWrapper::mock(), TaskCb(nullptr)) + .Times(1) + .WillOnce( + InvokeWithoutArgs([&]() { + abort_tasks(pool_); // to break the thread loop + return ret_val; + }) + ); + EXPECT_CALL(CbWrapper::mock(), OnTaskEndCb(task->id, task->param, ret_val)) + .Times(1); + + auto ret = thread_func(pool_); + EXPECT_EQ(ret, nullptr); + + // cleanup + destroy_task(task); +} + +/** + * @brief Given the pool has one task with callbacks with 'auto release' set, + * when the task is scheduled for execution, then the task's callbacks must be + * called at the suitable times and the task must be destroyed. + */ +TEST_F(PoolDayTest, ExecuteTaskWithCallbacksAndAutoRelease) { + auto task = create_async_task(0, CbWrapper::TaskCb, nullptr, 0, true, + CbWrapper::OnTaskStartCb, + CbWrapper::OnTaskEndCb); + char ret_val[]{ "hello, i'm the return of the task" }; + + enqueue_task(pool_, task); + + EXPECT_CALL(CbWrapper::mock(), OnTaskStartCb(task->id, task->param)) + .Times(1); + EXPECT_CALL(CbWrapper::mock(), TaskCb(nullptr)) + .Times(1) + .WillOnce( + InvokeWithoutArgs([&]() { + abort_tasks(pool_); // to break the thread loop + return ret_val; + }) + ); + EXPECT_CALL(CbWrapper::mock(), OnTaskEndCb(task->id, task->param, ret_val)) + .Times(1); + + auto ret = thread_func(pool_); + EXPECT_EQ(ret, nullptr); +} + +/** + * @brief Given the pool has one task with only start callback, when the task is + * scheduled for execution, then the task's start callback must be called. + */ +TEST_F(PoolDayTest, ExecuteTaskWithStartCallbackOnly) { + auto task = create_async_task(0, CbWrapper::TaskCb, nullptr, 0, false, + CbWrapper::OnTaskStartCb, nullptr); + char ret_val[]{ "hello, i'm the return of the task" }; + + enqueue_task(pool_, task); + + EXPECT_CALL(CbWrapper::mock(), OnTaskStartCb(task->id, task->param)) + .Times(1); + EXPECT_CALL(CbWrapper::mock(), TaskCb(nullptr)) + .Times(1) + .WillOnce( + InvokeWithoutArgs([&]() { + abort_tasks(pool_); // to break the thread loop + return ret_val; + }) + ); + EXPECT_CALL(CbWrapper::mock(), OnTaskEndCb(_, _, _)) + .Times(0); + + auto ret = thread_func(pool_); + EXPECT_EQ(ret, nullptr); + EXPECT_EQ(task->ret_val, ret_val); + + // cleanup + destroy_task(task); +} + +/** + * @brief Given the pool has one task with only end callback, when the task is + * scheduled for execution, then the task's end callback must be called. + */ +TEST_F(PoolDayTest, ExecuteTaskWithEndCallbackOnly) { + auto task = create_async_task(0, CbWrapper::TaskCb, nullptr, 0, false, + nullptr, CbWrapper::OnTaskEndCb); + char ret_val[]{ "hello, i'm the return of the task" }; + + enqueue_task(pool_, task); + + EXPECT_CALL(CbWrapper::mock(), OnTaskStartCb(_, _)) + .Times(0); + EXPECT_CALL(CbWrapper::mock(), TaskCb(nullptr)) + .Times(1) + .WillOnce( + InvokeWithoutArgs([&]() { + abort_tasks(pool_); // to break the thread loop + return ret_val; + }) + ); + EXPECT_CALL(CbWrapper::mock(), OnTaskEndCb(task->id, task->param, ret_val)) + .Times(1); + + auto ret = thread_func(pool_); + EXPECT_EQ(ret, nullptr); + + // cleanup + destroy_task(task); } /** @@ -197,11 +352,15 @@ TEST_F(PoolDayTest, ExecuteTaskWithNullParameterWithSuccess) { TEST_F(PoolDayTest, ExecuteTaskWithParameterWithSuccess) { char param[]{ "param" }; char ret_val[]{ "hello, i'm the return of the task" }; - auto task = create_task(CbWrapper::TaskCb, param); + auto task = create_async_task(0, CbWrapper::TaskCb, param, + sizeof(char) * strlen(param) + 1, false, + nullptr, nullptr); enqueue_task(pool_, task); - EXPECT_CALL(CbWrapper::mock(), TaskCb(param)) + EXPECT_CALL(CbWrapper::mock(), OnTaskStartCb(_, _)) + .Times(0); + EXPECT_CALL(CbWrapper::mock(), TaskCb(StrEqVoidPointer(param))) .Times(1) .WillOnce( InvokeWithoutArgs([&]() { @@ -209,13 +368,15 @@ TEST_F(PoolDayTest, ExecuteTaskWithParameterWithSuccess) { return ret_val; }) ); + EXPECT_CALL(CbWrapper::mock(), OnTaskEndCb(_, _, _)) + .Times(0); auto ret = thread_func(pool_); EXPECT_EQ(ret, nullptr); EXPECT_EQ(task->ret_val, ret_val); // cleanup - free(task); + destroy_task(task); } /** @@ -223,10 +384,9 @@ TEST_F(PoolDayTest, ExecuteTaskWithParameterWithSuccess) { * task is scheduled for execution, then the task's callback must not be called. */ TEST_F(PoolDayTest, ExecuteTaskWithMustStopSet) { - { - auto task = create_task(CbWrapper::TaskCb, nullptr); - enqueue_task(pool_, task); - } + auto task = create_sync_task(0, CbWrapper::TaskCb, nullptr, 0); + + enqueue_task(pool_, task); EXPECT_CALL(CbWrapper::mock(), TaskCb(_)).Times(0); @@ -234,6 +394,9 @@ TEST_F(PoolDayTest, ExecuteTaskWithMustStopSet) { auto ret = thread_func(pool_); EXPECT_EQ(ret, nullptr); + + // to force the task destruction on destroy_pool call + task->is_orphan = true; } /** @@ -248,15 +411,18 @@ TEST_F(PoolDayTest, AbortTasksWithNullPoolHandle) { * @brief Given we have an enqueued task, when we wait for the finish of the * task, then the correct return value must be returned with success. */ -TEST_F(PoolDayTest, WaitTaskFinish) { +TEST_F(PoolDayTest, GetTaskResultWithSuccess) { int ret_val{1234}; char param[]{ "param" }; - auto task = create_task(CbWrapper::TaskCb, param); + auto task = create_sync_task(0, CbWrapper::TaskCb, param, + sizeof(char) * strlen(param) + 1); { enqueue_task(pool_, task); - EXPECT_CALL(CbWrapper::mock(), TaskCb(param)) + EXPECT_CALL(CbWrapper::mock(), OnTaskStartCb(_, _)) + .Times(0); + EXPECT_CALL(CbWrapper::mock(), TaskCb(StrEqVoidPointer(param))) .Times(1) .WillOnce( Invoke([&]() { @@ -264,31 +430,51 @@ TEST_F(PoolDayTest, WaitTaskFinish) { return &ret_val; }) ); + EXPECT_CALL(CbWrapper::mock(), OnTaskEndCb(_, _, _)) + .Times(0); thread_func(pool_); } - auto ret = reinterpret_cast(wait_task_finish(pool_, task)); + auto ret = reinterpret_cast(get_task_result(task)); EXPECT_EQ(*ret, ret_val); + + // cleanup + destroy_task(task); +} + +/** + * @brief Given we have a null task and a valid pool handle, when we wait for + * the finish of the task, then null must be returned. + */ +TEST_F(PoolDayTest, GetTaskResultWithNullPoolTask) { + EXPECT_EQ(get_task_result(nullptr), nullptr); } /** - * @brief Given we have a task and a null pool handle, when we wait for + * @brief Given we have a task not bound to the pool, when we wait for * the finish of the task, then null must be returned. */ -TEST_F(PoolDayTest, WaitTaskFinishWithNullPoolHandle) { - auto task = create_task(nullptr, nullptr); +TEST_F(PoolDayTest, GetTaskResultWithWithUnboundTask) { + auto task = create_sync_task(0, nullptr, nullptr, 0); - EXPECT_EQ(wait_task_finish(nullptr, task), nullptr); + EXPECT_EQ(get_task_result(task), nullptr); // cleanup - free(task); + destroy_task(task); } /** - * @brief Given we have a null task and a valid pool handle, when we wait for - * the finish of the task, then null must be returned. + * @brief Given we have a task with the 'auto_release' option set, when we wait + * for the finish of the task, then null must be returned. */ -TEST_F(PoolDayTest, WaitTaskFinishWithNullPoolTask) { - EXPECT_EQ(wait_task_finish(pool_, nullptr), nullptr); +TEST_F(PoolDayTest, GetTaskResultWithAutoReleaseSet) { + auto task = create_async_task(0, nullptr, nullptr, 0, true, + CbWrapper::OnTaskStartCb, + CbWrapper::OnTaskEndCb); + + EXPECT_EQ(get_task_result(task), nullptr); + + // cleanup + destroy_task(task); } diff --git a/test/src/queue_test.cc b/test/src/queue_test.cc index d859f69..cc4221b 100644 --- a/test/src/queue_test.cc +++ b/test/src/queue_test.cc @@ -2,7 +2,7 @@ extern "C" { #include "internal/queue.h" -#include "internal/task.h" +#include "task.h" } class QueueTest : public ::testing::Test { @@ -33,11 +33,11 @@ TEST_F(QueueTest, GetQueueSizeWithEmptyQueue) { */ TEST_F(QueueTest, GetQueueSizeWithNotEmptyQueue) { { - auto t1 = create_task(nullptr, nullptr); - auto t2 = create_task(nullptr, nullptr); - auto t3 = create_task(nullptr, nullptr); - auto t4 = create_task(nullptr, nullptr); - auto t5 = create_task(nullptr, nullptr); + auto t1 = create_sync_task(0, nullptr, nullptr, 0); + auto t2 = create_sync_task(1, nullptr, nullptr, 0); + auto t3 = create_sync_task(2, nullptr, nullptr, 0); + auto t4 = create_sync_task(3, nullptr, nullptr, 0); + auto t5 = create_sync_task(4, nullptr, nullptr, 0); enqueue(queue_, t1); enqueue(queue_, t2); @@ -63,8 +63,8 @@ TEST_F(QueueTest, GetQueueSizeWithNullQueue) { */ TEST_F(QueueTest, EnqueueElementWithNotEmptyQueue) { { - auto t1 = create_task(nullptr, nullptr); - auto t2 = create_task(nullptr, nullptr); + auto t1 = create_sync_task(0, nullptr, nullptr, 0); + auto t2 = create_sync_task(1, nullptr, nullptr, 0); enqueue(queue_, t1); enqueue(queue_, t2); @@ -72,7 +72,7 @@ TEST_F(QueueTest, EnqueueElementWithNotEmptyQueue) { EXPECT_EQ(queue_size(queue_), 2); } - auto t3 = create_task(nullptr, nullptr); + auto t3 = create_sync_task(3, nullptr, nullptr, 0); enqueue(queue_, t3); @@ -95,8 +95,8 @@ TEST_F(QueueTest, EnqueueNullElementWithEmptyQueue) { */ TEST_F(QueueTest, EnqueueNullElementWithNotEmptyQueue) { { - auto t1 = create_task(nullptr, nullptr); - auto t2 = create_task(nullptr, nullptr); + auto t1 = create_sync_task(0, nullptr, nullptr, 0); + auto t2 = create_sync_task(1, nullptr, nullptr, 0); enqueue(queue_, t1); enqueue(queue_, t2); @@ -112,7 +112,7 @@ TEST_F(QueueTest, EnqueueNullElementWithNotEmptyQueue) { * then nothing must happen. */ TEST_F(QueueTest, EnqueueElementWithNullQueue) { - auto t1 = create_task(nullptr, nullptr); + auto t1 = create_sync_task(0, nullptr, nullptr, 0); enqueue(nullptr, t1); free(t1); @@ -138,9 +138,9 @@ TEST_F(QueueTest, DequeueElementWithEmptyQueue) { * size of the queue must be decreased by 1. */ TEST_F(QueueTest, DequeueSingleElementWithNotEmptyQueue) { - auto t1 = create_task(nullptr, nullptr); - auto t2 = create_task(nullptr, nullptr); - auto t3 = create_task(nullptr, nullptr); + auto t1 = create_sync_task(0, nullptr, nullptr, 0); + auto t2 = create_sync_task(1, nullptr, nullptr, 0); + auto t3 = create_sync_task(2, nullptr, nullptr, 0); enqueue(queue_, t1); enqueue(queue_, t2); @@ -163,11 +163,11 @@ TEST_F(QueueTest, DequeueSingleElementWithNotEmptyQueue) { * the operations. */ TEST_F(QueueTest, DequeueMultipleElementsWithNotEmptyQueue) { - auto t1 = create_task(nullptr, nullptr); - auto t2 = create_task(nullptr, nullptr); - auto t3 = create_task(nullptr, nullptr); - auto t4 = create_task(nullptr, nullptr); - task_t *ret; + auto t1 = create_sync_task(0, nullptr, nullptr, 0); + auto t2 = create_sync_task(1, nullptr, nullptr, 0); + auto t3 = create_sync_task(2, nullptr, nullptr, 0); + auto t4 = create_sync_task(3, nullptr, nullptr, 0); + task_t ret; enqueue(queue_, t1); enqueue(queue_, t2);