diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt index ab1b5b60a4..3718043f49 100644 --- a/libraries/CMakeLists.txt +++ b/libraries/CMakeLists.txt @@ -8,7 +8,7 @@ add_custom_target(newlib-headers ${CMAKE_COMMAND} -E copy_directory "${NEWLIB_SRC_DIR}/newlib/libc/include" "${KOS_SDK_DIR}/i586-kolibrios/include" - COMMENT "Copying all Newlib headers to ${KOS_SDK_DIR}}/include" + COMMENT "Copying all Newlib headers to ${KOS_SDK_DIR}/include" ) # Newlib @@ -25,3 +25,14 @@ ExternalProject_Add( BUILD_ALWAYS TRUE DEPENDS gcc ) + +# kospthread +ExternalProject_Add( + kospthread + SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/kospthread + CMAKE_ARGS + -DCMAKE_TOOLCHAIN_FILE=${KOS_TOOLCHAIN_FILE} + -DCMAKE_INSTALL_PREFIX=${KOS_SDK_DIR}/i586-kolibrios + BUILD_ALWAYS TRUE + DEPENDS gcc +) diff --git a/libraries/kospthread/CMakeLists.txt b/libraries/kospthread/CMakeLists.txt new file mode 100644 index 0000000000..351fe85d4d --- /dev/null +++ b/libraries/kospthread/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.31) + +project(kospthread LANGUAGES C ASM) + +# libpthread +add_library(pthread + src/threads.c + src/exit.S +) + +install(TARGETS pthread DESTINATION lib) diff --git a/libraries/kospthread/README.md b/libraries/kospthread/README.md new file mode 100644 index 0000000000..2754b35021 --- /dev/null +++ b/libraries/kospthread/README.md @@ -0,0 +1,57 @@ +# kospthread + +This is a minimal implementation of POSIX threads [IEEE Std 1003.1-2024](https://pubs.opengroup.org/onlinepubs/9799919799/) for KolibriOS. + +## Main goal + +Implement the minimum set of functions for `libgcc` and `libstdc++` to work: + +## Status + +Due to the way threads work in **KolibriOS**, the current **pthread** only implements the ability to spawn threads from the main thread. +An attempt to create a child thread will result in an `EAGAIN` error. + +Fully implemented: + +- `pthread_equal()` +- `pthread_self()` + +Implemented with restrictions: + +- `pthread_create()`: attributes are not supported; only main thread. +- `pthread_join()`: only main thread; + +Not implemented: + +- `pthread_once()` +- `pthread_detach()` +- `sched_yield()` + +- `pthread_mutex_lock()` +- `pthread_mutex_trylock()` +- `pthread_mutex_unlock()` +- `pthread_mutex_init()` +- `pthread_mutex_destroy()` + +- `pthread_mutexattr_init()` +- `pthread_mutexattr_settype()` +- `pthread_mutexattr_destroy()` + +- `pthread_cond_init()` +- `pthread_cond_broadcast()` +- `pthread_cond_signal()` +- `pthread_cond_wait()` +- `pthread_cond_timedwait()` +- `pthread_cond_destroy()` + +- `pthread_key_create()` +- `pthread_key_delete()` +- `pthread_getspecific()` +- `pthread_setspecific()` + +## Build + +```sh +cmake -B build -DCMAKE_TOOLCHAIN_FILE=${KOS_TOOLCHAIN_FILE} +cmake --build build +``` diff --git a/libraries/kospthread/src/exit.S b/libraries/kospthread/src/exit.S new file mode 100644 index 0000000000..b82585e3fd --- /dev/null +++ b/libraries/kospthread/src/exit.S @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: GPL-2.0-only + * Copyright (C) 2026 KolibriOS team + * Author: Maxim Logaev + */ + +#include +#include + +.section .text + +.global ___pthread_child_free_stk_and_exit + +___pthread_child_free_stk_and_exit: + + movl %fs:KOS_TLS_OFF_PTHREAD_SELF, %eax + movl (%eax), %ecx + movl $SF_SYS_MISC, %eax + movl $SSF_MEM_FREE, %ebx + int $0x40 + orl $SF_TERMINATE_PROCESS, %eax + int $0x40 diff --git a/libraries/kospthread/src/threads.c b/libraries/kospthread/src/threads.c new file mode 100644 index 0000000000..a89454cd2e --- /dev/null +++ b/libraries/kospthread/src/threads.c @@ -0,0 +1,293 @@ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Basic POSIX thread wrappers for KolibriOS. + * + * Copyright (C) 2026 KolibriOS team + * Author: Maxim Logaev + */ + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +extern __dead2 void __pthread_child_free_stk_and_exit (void); + +#define CHILDREN_MAX (KOS_THREADS_MAX - 1) // -1 self + +#define PUSH_PTR(sp, ptr) \ + do \ + { \ + sp -= sizeof (void *); \ + *(void **)sp = ptr; \ + } \ + while (0) + +#define IS_VALID_CHILD_ID(id) (id < CHILDREN_MAX) +#define MAIN_THREAD_ID -1 + +#define THREAD_STATUS_NORMAL_EXIT 1 + +struct pthread_self +{ + void *stack_lo; // Do not change the order! (see exit.S) + unsigned long status; + pthread_t id; + void *ret; +}; + +struct pthread_child +{ + unsigned long kos_tid; + struct pthread_self self; +}; + +static bool inited; +static pthread_t next_child_id; +static struct pthread_child children[CHILDREN_MAX]; + +/* --------------------------- Helper functions ---------------------------- */ + +static bool +free_stack_if_dead (pthread_t id) +{ + /* + * The child must free his stack upon correct exit. + * If there is a crash in the child, then the main thread will have to do it. + */ + if (children[id].self.stack_lo != (void *)-1 + && children[id].self.status != THREAD_STATUS_NORMAL_EXIT) + { + _ksys (SF_SYS_MISC, SSF_MEM_FREE, + (unsigned long)children[id].self.stack_lo); + children[id].self.stack_lo = (void *)-1; + + return true; + } + + return false; +} + +static pthread_t +alloc_child (void) +{ + // First pass: start with the next one + for (pthread_t id = next_child_id; id < CHILDREN_MAX; id++) + { + if (children[id].kos_tid == -1) + { + next_child_id = id + 1; + return id; + } + } + + // Second pass: start from the beginning to the last + for (pthread_t id = 0; id < next_child_id; id++) + { + if (children[id].kos_tid == -1) + { + next_child_id = id + 1; + return id; + } + } + + // Third pass: search for the dead + for (pthread_t id = 0; id < CHILDREN_MAX; id++) + { + if (!_ksys (SF_SYSTEM, SSF_GET_THREAD_SLOT, children[id].kos_tid)) + { + free_stack_if_dead (id); + next_child_id = id + 1; + return id; + } + } + + return (pthread_t)-1; +} + +static inline void +clean_child (pthread_t id) +{ + __builtin_memset (&children[id], -1, sizeof (children[id])); +} + +static inline bool +is_child (void) +{ + return __kos_tls_get (KOS_TLS_OFF_PTHREAD_SELF) != NULL; +} + +static inline void +__pthread_main_init_once (void) +{ + if (!inited) + { + next_child_id = 0; + __builtin_memset (children, -1, sizeof (children)); + inited = true; + } +} + +static __dead2 void +__pthread_main_exit (void) +{ + if (inited) + { + for (pthread_t id = 0; id < CHILDREN_MAX; id++) + { + if (children[id].kos_tid != -1) + { + _ksys (SF_SYSTEM, SSF_TERMINATE_THREAD_ID, children[id].kos_tid); + } + } + } + + /* Freeing the children stack is pointless when the main is terminated */ + _ksys (SF_TERMINATE_PROCESS); + __unreachable (); +} + +static __dead2 void +__pthread_child_exit (struct pthread_self *self, void *ret) +{ + self->status = THREAD_STATUS_NORMAL_EXIT; + self->ret = ret; + + __pthread_child_free_stk_and_exit (); + __unreachable (); +} + +static __dead2 void +__thread_func (void *self, void *(*pthread_fn) (void *), void *arg) +{ + __kos_tls_set (KOS_TLS_OFF_PTHREAD_SELF, self); + + void *ret = pthread_fn (arg); + + __pthread_child_exit (self, ret); + __unreachable (); +} + +/* ----------------------------- Main functions ---------------------------- */ + +void __dead2 +pthread_exit (void *ret) +{ + void *self = __kos_tls_get (KOS_TLS_OFF_PTHREAD_SELF); + if (self) + { + __pthread_child_exit (__kos_tls_get (KOS_TLS_OFF_PTHREAD_SELF), ret); + __unreachable (); + } + + __pthread_main_exit (); + __unreachable (); +} + +int +pthread_create (pthread_t *id, const pthread_attr_t *attr, + void *(*func) (void *), void *arg) +{ + if (attr) + return EINVAL; + + __pthread_main_init_once (); + + if (is_child ()) + return EAGAIN; + + pthread_t child_id = alloc_child (); + if (!IS_VALID_CHILD_ID (child_id)) + return EAGAIN; + + // Allocate memory for new stack. Is freed by the new thread itself. + char *stack_lo + = (char *)_ksys (SF_SYS_MISC, SSF_MEM_ALLOC, __kosapp_stack_size); + if (!stack_lo) + return EAGAIN; + + // Prepare a new stack: + char *stack_hi = stack_lo + __kosapp_stack_size; + + children[child_id].self.id = child_id; + children[child_id].self.stack_lo = stack_lo; + + // Push arguments + PUSH_PTR (stack_hi, arg); + PUSH_PTR (stack_hi, func); + PUSH_PTR (stack_hi, &children[child_id].self); + + // Push unreachable return address + PUSH_PTR (stack_hi, (void *)-1); + + // Create thread: + unsigned long tid = _ksys (SF_THREAD_CTRL, SSF_CREATE_THREAD, + (intptr_t)__thread_func, (intptr_t)stack_hi); + if (tid == -1) + { + _ksys (SF_SYS_MISC, SSF_MEM_FREE, (intptr_t)stack_lo); + return EAGAIN; + } + + children[child_id].kos_tid = tid; + *id = child_id; + + return 0; +} + +int +pthread_join (pthread_t id, void **ret) +{ + if (is_child () || id == MAIN_THREAD_ID) + return EDEADLK; + + if (!IS_VALID_CHILD_ID (id)) + return ESRCH; + + while (_ksys (SF_SYSTEM, SSF_GET_THREAD_SLOT, children[id].kos_tid)) + { + // Sleep 1s + _ksys (SF_SLEEP, 100); + } + + void *save_ret = children[id].self.ret; + + bool is_dead = free_stack_if_dead (id); + clean_child (id); + + // If the thread terminated unexpectedly + if (is_dead) + return ESRCH; + + if (ret) + *ret = save_ret; + + return 0; +} + +pthread_t +pthread_self (void) +{ + const struct pthread_self *self = __kos_tls_get (KOS_TLS_OFF_PTHREAD_SELF); + if (self) + { + return self->id; + } + + return MAIN_THREAD_ID; +} + +int +pthread_equal (pthread_t __t1, pthread_t __t2) +{ + return __t1 == __t2; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 785a8d6b9a..fd4f2939fc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,9 +1,11 @@ cmake_minimum_required(VERSION 3.31) -project(tests) +project(tests C) add_executable(hello hello.c) add_executable(malloc malloc.c) -install(TARGETS hello DESTINATION tests) -install(TARGETS malloc DESTINATION tests) +add_executable(pthread_create pthread.c) +target_link_libraries(pthread_create PRIVATE pthread) + +install(TARGETS hello malloc pthread_create DESTINATION tests) diff --git a/tests/pthread.c b/tests/pthread.c new file mode 100644 index 0000000000..20c80e5be2 --- /dev/null +++ b/tests/pthread.c @@ -0,0 +1,119 @@ +#include +#include +#include +#include + +#include + +void * +thread (void *arg) +{ + if (arg == (void *)0xDEADBEEF) + { + _ksys_dbg_print ("I: Thread1: Valid argument\n"); + } + + _ksys (SF_SLEEP, 1000); + + char *str = "I: Thread1: Ok!\n"; + return (void *)str; +} + +void * +thread_exit (void *arg) +{ + if (arg == (void *)0xBEEFDEAD) + { + _ksys_dbg_print ("I: Thread2: Valid argument\n"); + } + + _ksys (SF_SLEEP, 1000); + + char *str = "I: Thread2: Ok!\n"; + pthread_exit (str); +} + +void * +thread_crash (void *arg) +{ + if (arg == (void *)0xDEADCAFE) + { + _ksys_dbg_print ("I: Thread3: Valid argument\n"); + } + + _ksys (SF_SLEEP, 1000); + + /* Crash: General protection fault! */ + __asm__ ("int3"); + + char *str = "E: Joinded 3: Fail!\n"; + return str; +} + +int +main () +{ + int rc = 0; + pthread_t id1, id2, id3; + void *ret = NULL; + + rc = pthread_create (&id1, NULL, thread, (void *)0xDEADBEEF); + if (rc) + { + fprintf (stderr, "E: pthread_create() failed: %s\n", strerror (rc)); + return 1; + } + + _ksys_dbg_print ("I: Created 1\n"); + + rc = pthread_create (&id2, NULL, thread_exit, (void *)0xBEEFDEAD); + if (rc) + { + fprintf (stderr, "E: pthread_create() failed: %s\n", strerror (rc)); + return 1; + } + + _ksys_dbg_print ("I: Created 2\n"); + + rc = pthread_create (&id3, NULL, thread_crash, (void *)0xDEADCAFE); + if (rc) + { + fprintf (stderr, "E: pthread_create() failed: %s\n", strerror (rc)); + return 1; + } + + _ksys_dbg_print ("I: Created 3\n"); + + rc = pthread_join (id1, &ret); + if (rc) + { + fprintf (stderr, "E: pthread_join() failed: %s\n", strerror (rc)); + return 1; + } + + _ksys_dbg_print ("I: Joinded 1\n"); + _ksys_dbg_print (ret); + + rc = pthread_join (id2, &ret); + if (rc) + { + fprintf (stderr, "E: pthread_join() failed: %s\n", strerror (rc)); + return 1; + } + + _ksys_dbg_print ("I: Joinded 2\n"); + _ksys_dbg_print (ret); + + rc = pthread_join (id3, &ret); + if (rc == ESRCH) + { + _ksys_dbg_print ("I: Not joined 3 (negative tests)\n"); + _ksys_dbg_print ("I: Thread3: Ok!\n"); + return 0; + } + + _ksys_dbg_print ("E: Joinded 3\n"); + _ksys_dbg_print (ret); + + return 1; +}