diff --git a/CMakeLists.txt b/CMakeLists.txt index 472df57..e89c6e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ add_subdirectory(extern/benchmark_base) add_subdirectory(extern/benchmark_runner) # Include all internal subprojects (library, examples, testing). +add_subdirectory(lib/context_switcher) add_subdirectory(lib/pls) # Include examples diff --git a/app/context_switch/main.cpp b/app/context_switch/main.cpp index e77b555..b87d5f8 100644 --- a/app/context_switch/main.cpp +++ b/app/context_switch/main.cpp @@ -15,7 +15,7 @@ const size_t STACK_SIZE = 512 * 1; const char MAGIC_NUMBER = (unsigned char) 0xAB; // Memory for custom stack and continuation semantics -char custom_stack[STACK_SIZE] = {0}; +char custom_stack_1[STACK_SIZE] = {0}; jmp_buf buffer; // Example callback function and declaration of our assembly stack switching routine @@ -50,7 +50,7 @@ long measure_function_call() { long measure_stack_switch() { auto start_time = chrono::steady_clock::now(); for (unsigned int i = 0; i < NUM_RUNS; i++) { - custom_stack_callback(&custom_stack[STACK_SIZE]); + custom_stack_callback(&custom_stack_1[STACK_SIZE]); } auto end_time = chrono::steady_clock::now(); return chrono::duration_cast(end_time - start_time).count(); @@ -60,7 +60,7 @@ long measure_continuation() { auto start_time = chrono::steady_clock::now(); for (unsigned int i = 0; i < NUM_RUNS; i++) { if (setjmp(buffer) == 0) { - custom_stack_callback(&custom_stack[STACK_SIZE]); + custom_stack_callback(&custom_stack_1[STACK_SIZE]); } } auto end_time = chrono::steady_clock::now(); @@ -71,7 +71,7 @@ long measure_continuation_and_jump() { auto start_time = chrono::steady_clock::now(); for (unsigned int i = 0; i < NUM_RUNS; i++) { if (setjmp(buffer) == 0) { - custom_stack_callback(&custom_stack[STACK_SIZE]); + custom_stack_callback(&custom_stack_1[STACK_SIZE]); longjmp(buffer, 1); } } @@ -87,7 +87,7 @@ void fcontext_callback_fast(fcontext_transfer_t transfer) { } long measure_fcontext_fast() { - fcontext_t context = make_fcontext(&custom_stack[STACK_SIZE], STACK_SIZE, &fcontext_callback_fast); + fcontext_t context = make_fcontext(&custom_stack_1[STACK_SIZE], STACK_SIZE, &fcontext_callback_fast); auto start_time = chrono::steady_clock::now(); for (unsigned int i = 0; i < NUM_RUNS; i++) { @@ -105,7 +105,7 @@ void fcontext_callback_clean(fcontext_transfer_t transfer) { long measure_fcontext_clean() { auto start_time = chrono::steady_clock::now(); for (unsigned int i = 0; i < NUM_RUNS; i++) { - fcontext_t context = make_fcontext(&custom_stack[STACK_SIZE], STACK_SIZE, &fcontext_callback_clean); + fcontext_t context = make_fcontext(&custom_stack_1[STACK_SIZE], STACK_SIZE, &fcontext_callback_clean); jump_fcontext(context, nullptr); } auto end_time = chrono::steady_clock::now(); @@ -120,7 +120,7 @@ void fcontext_callcc(fcontext_transfer_t transfer) { long measure_fcontext_callcc() { auto start_time = chrono::steady_clock::now(); for (unsigned int i = 0; i < NUM_RUNS; i++) { - fcontext_t context = make_fcontext(&custom_stack[STACK_SIZE], STACK_SIZE, &fcontext_callcc); + fcontext_t context = make_fcontext(&custom_stack_1[STACK_SIZE], STACK_SIZE, &fcontext_callcc); jump_fcontext(jump_fcontext(context, nullptr).ctx, nullptr); } auto end_time = chrono::steady_clock::now(); @@ -132,7 +132,7 @@ long measure_custom() { auto start_time = chrono::steady_clock::now(); for (unsigned int i = 0; i < NUM_RUNS; i++) { - fiber_call(custom_stack, STACK_SIZE, [](continuation_t continuation) { + fiber_call(custom_stack_1, STACK_SIZE, [](continuation_t continuation) { callback(); return continuation; }); @@ -142,7 +142,7 @@ long measure_custom() { } int main() { - memset(custom_stack, MAGIC_NUMBER, STACK_SIZE); + memset(custom_stack_1, MAGIC_NUMBER, STACK_SIZE); auto time_cont_jump = measure_continuation_and_jump(); auto time_cont = measure_continuation(); @@ -169,7 +169,7 @@ int main() { printf("Custom Fast Call : %10ld, %5.5f\n", time_custom, ((float) time_custom / NUM_RUNS)); for (unsigned int i = 0; i < STACK_SIZE; i++) { - if (custom_stack[i] != MAGIC_NUMBER) { + if (custom_stack_1[i] != MAGIC_NUMBER) { printf("\n\nUsed stack size about %u bytes.\n", (STACK_SIZE - i)); break; } diff --git a/app/playground/CMakeLists.txt b/app/playground/CMakeLists.txt index 4c09641..6673f70 100644 --- a/app/playground/CMakeLists.txt +++ b/app/playground/CMakeLists.txt @@ -1,4 +1,4 @@ add_executable(playground main.cpp) # Example for adding the library to your app (as a cmake project dependency) -target_link_libraries(playground) +target_link_libraries(playground context_switcher) diff --git a/app/playground/main.cpp b/app/playground/main.cpp index a1c17b1..38b41fd 100644 --- a/app/playground/main.cpp +++ b/app/playground/main.cpp @@ -1,9 +1,27 @@ +#include #include -#include + +#include "context_switcher/context_switcher.h" + +// Memory for custom stack and continuation semantics +const size_t STACK_SIZE = 512 * 8; +char custom_stack_1[STACK_SIZE]; + +// Force disable optimization +volatile int value = 0; int main() { - printf("Buffer Size: %u\n", sizeof(jmp_buf)); - printf("Big Buffer Size: %u\n", sizeof(sigjmp_buf)); + using namespace context_switcher; + printf("Main 1!\n"); + auto cont_2 = enter_context(custom_stack_1, STACK_SIZE, [](continuation &&cont_main) { + printf("Stack 1!\n"); + cont_main = switch_context(std::move(cont_main)); + printf("Stack 2!\n"); + return std::move(cont_main); + }); + printf("Main 2!\n"); + cont_2 = switch_context(std::move(cont_2)); + printf("Main 3!\n"); return 0; } diff --git a/lib/context_switcher/CMakeLists.txt b/lib/context_switcher/CMakeLists.txt new file mode 100644 index 0000000..b1c6663 --- /dev/null +++ b/lib/context_switcher/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.10) +project(context_switcher + VERSION 0.0.1 + DESCRIPTION "allows to execute functions and lambdas on a new stack and switch between them" + LANGUAGES CXX ASM) + +set(CMAKE_CXX_STANDARD 11) + +# Platform Support - Edit this when porting the context switch facility. +# We are rather conservative with our flags, maybe more systems follow implemented calling conventions. +# See our documentation and boost context for help when adding a new system. +if (CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + SET(CONTEXT_SWITCH_ASSEMBLY "asm/enter_context_x86_64.s" "asm/switch_context_x86_64.s") +elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "armv7l" AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + SET(CONTEXT_SWITCH_ASSEMBLY "asm/enter_context_arm32.s" "asm/switch_context_arm32.s") +else () + MESSAGE(FATAL_ERROR "Platform (${CMAKE_SYSTEM_PROCESSOR} on ${CMAKE_SYSTEM_NAME}) not supported! Please see Readme for instructions to port.") +endif () +message("-- Context Switcher: ${CMAKE_SYSTEM_PROCESSOR} running ${CMAKE_SYSTEM_NAME}") + +add_library(context_switcher STATIC + ${CONTEXT_SWITCH_ASSEMBLY} + include/context_switcher/context_switcher.h src/context_switcher.cpp include/context_switcher/assembly_bindings.h include/context_switcher/continuation.h include/context_switcher/lambda_capture.h) + +# Add everything in `./include` to be in the include path of this project +target_include_directories(context_switcher + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + diff --git a/lib/context_switcher/asm/enter_context_arm32.s b/lib/context_switcher/asm/enter_context_arm32.s new file mode 100644 index 0000000..953531b --- /dev/null +++ b/lib/context_switcher/asm/enter_context_arm32.s @@ -0,0 +1,53 @@ + .arm + .text + .global __cs_enter_context + .type __cs_enter_context, %function + +__cs_enter_context: + /* Parameter List (in order) + * r0 = new stack pointer + * r1 = first parameter to callback + * r2 = callback function pointer + * r3 = new stack limit (not used on most platforms) + * + * Return + * r0 = continuation that returned control back to the caller (null if fallthrough) + * + * Variables + * r4 = temporary for the old stack pointer */ + + /* ========== Save State ========== */ + /* store programm counter for later return */ + push {lr} + /* store callee saved registers */ + push {r4-r12,lr} + /* ========== Save State ========== */ + + /* Perform change to new stack */ + /* Keep old stack as second parameter to our callback function. */ + mov r4, sp + /* Make sure that stack start is properly aligned. */ + and r0, r0, #-16 + /* Switch to new stack pointer. */ + mov sp, r0 + + /* Perform actual function call, this will now be on the new stack */ + /* r0 = first parametor to callback (continuation) */ + /* r1 = second parameter to callback (arbetary pointer) */ + mov r0, r4 + blx r2 + + /* Restore state of returned continuation. */ + /* To do so we first reset the stack pointer (which we get returned in r0). */ + /* After that we execute our standard restore procedere to pop the state from the stack. */ + mov sp, r0 + + /* ========== Restore State ========== */ + /* restore callee saved registers */ + pop {r4-r12,lr} + /* ========== Restore State ========== */ + + /* Just return back from the call. */ + /* This is the end of a fiber, so we have no continuation. */ + eor r0, r0, r0 + pop {pc} diff --git a/lib/context_switcher/asm/enter_context_x86_64.s b/lib/context_switcher/asm/enter_context_x86_64.s new file mode 100644 index 0000000..58afc40 --- /dev/null +++ b/lib/context_switcher/asm/enter_context_x86_64.s @@ -0,0 +1,78 @@ + .file "enter_context_x86_64.s" + .text + .global __cs_enter_context + .type __cs_enter_context, @function + +.align 16 +__cs_enter_context: + # Parameter List (in order) + # rdi = new stack pointer + # rsi = first parameter to callback + # rdx = callback function pointer + # rcx = new stack limit (not used on most platforms) + + # Return + # rax = continuation that returned control back to the caller (null if fallthrough) + + # Variables + # r12 = temporary for the old stack pointer + + ############### Save State ############### + # Make space for all register state we will store. + leaq -0x38(%rsp), %rsp + + # Store calee saved general registers. + movq %r12, 0x00(%rsp) + movq %r13, 0x08(%rsp) + movq %r14, 0x10(%rsp) + movq %r15, 0x18(%rsp) + movq %rbx, 0x20(%rsp) + movq %rbp, 0x28(%rsp) + # Store MMX control- and status-word + stmxcsr 0x30(%rsp) + # Store x87 control-word + fnstcw 0x34(%rsp) + ############### Save State ############### + + # Perform change to new stack. + # Keep old stack as second parameter to our callback function. + movq %rsp, %r12 + # Make sure that stack start is properly aligned. + andq $-16, %rdi + # Switch to new stack pointer. + movq %rdi, %rsp + + # Perform actual function call, this will now be on the new stack + # rdi = first parametor to callback (continuation) + # rsi = second parameter to callback (arbetary pointer) + movq %r12, %rdi + call *%rdx + + # Restore state of returned continuation. + # To do so we first reset the stack pointer (which we get returned in rax). + # After that we execute our standard restore procedere to pop the state from the stack. + movq %rax, %rsp + + ############ Restore State ############ + # restore calee saved general registers + movq 0x00(%rsp), %r12 + movq 0x08(%rsp), %r13 + movq 0x10(%rsp), %r14 + movq 0x18(%rsp), %r15 + movq 0x20(%rsp), %rbx + movq 0x28(%rsp), %rbp + # restore MMX control- and status-word + ldmxcsr 0x30(%rsp) + # restore x87 control-word + fldcw 0x34(%rsp) + + # Free space for restored state + leaq 0x38(%rsp), %rsp + ############ Restore State ############ + + # TODO: Maybe look into a 'cleanup' hook for freeing the stack space here. + + # Just return back from the call. + # This is the end of a fiber, so we have no continuation. + xor %rax, %rax + ret diff --git a/lib/context_switcher/asm/switch_context_arm32.s b/lib/context_switcher/asm/switch_context_arm32.s new file mode 100644 index 0000000..29d1831 --- /dev/null +++ b/lib/context_switcher/asm/switch_context_arm32.s @@ -0,0 +1,38 @@ + .arm + .text + .global __cs_switch_context + .type __cs_switch_context, %function + +__cs_switch_context: + /* Parameter List (in order) + * r0 = pointer to continuation (should hold value of target stack will be filled with this continuation) + * + * Return + * r0 = continuation that returned control back to the caller (null if fallthrough) + * + * Variables + * r1 = temporary for the old stack pointer */ + + /* ========== Save State ========== */ + /* store programm counter for later return */ + push {lr} + /* store callee saved registers */ + push {r4-r12,lr} + /* ========== Save State ========== */ + + /* Perform change to new stack */ + /* Keep old stack as result from this function. */ + mov r1, sp + /* Switch to new stack pointer. */ + mov sp, r0 + + + /* ========== Restore State ========== */ + /* restore callee saved registers */ + pop {r4-r12,lr} + /* ========== Restore State ========== */ + + /* Just return back from the call. */ + /* This is the end of a fiber, so we have no continuation. */ + mov r0, r1 + pop {pc} diff --git a/lib/context_switcher/asm/switch_context_x86_64.s b/lib/context_switcher/asm/switch_context_x86_64.s new file mode 100644 index 0000000..10d536e --- /dev/null +++ b/lib/context_switcher/asm/switch_context_x86_64.s @@ -0,0 +1,56 @@ + .file "switch_context_x86_64.s" + .text + .global __cs_switch_context + .type __cs_switch_context, @function + +.align 16 +__cs_switch_context: + # Parameter List (in order) + # rdi = pointer to continuation (should hold value of target stack will be filled with this continuation) + + # Return + # rax = continuation that returned control back to the caller (null if fallthrough) + + ############### Save State ############### + # Make space for all register state we will store. + leaq -0x38(%rsp), %rsp + + # Store calee saved general registers. + movq %r12, 0x00(%rsp) + movq %r13, 0x08(%rsp) + movq %r14, 0x10(%rsp) + movq %r15, 0x18(%rsp) + movq %rbx, 0x20(%rsp) + movq %rbp, 0x28(%rsp) + # Store MMX control- and status-word + stmxcsr 0x30(%rsp) + # Store x87 control-word + fnstcw 0x34(%rsp) + ############### Save State ############### + + # Perform change to new stack. + # Keep old stack as result from this function + movq %rsp, %rax + # switch to new stack pointer + movq %rdi, %rsp + + ############ Restore State ############ + # restore calee saved general registers + movq 0x00(%rsp), %r12 + movq 0x08(%rsp), %r13 + movq 0x10(%rsp), %r14 + movq 0x18(%rsp), %r15 + movq 0x20(%rsp), %rbx + movq 0x28(%rsp), %rbp + # restore MMX control- and status-word + ldmxcsr 0x30(%rsp) + # restore x87 control-word + fldcw 0x34(%rsp) + + # Free space for restored state + leaq 0x38(%rsp), %rsp + ############ Restore State ############ + + # Return the context we came from as a continuation. + # rax has already the correct value + ret diff --git a/lib/context_switcher/include/context_switcher/assembly_bindings.h b/lib/context_switcher/include/context_switcher/assembly_bindings.h new file mode 100644 index 0000000..e87576f --- /dev/null +++ b/lib/context_switcher/include/context_switcher/assembly_bindings.h @@ -0,0 +1,36 @@ + +#ifndef CONTEXT_SWITCHER_ASSEMBLY_BINDINGS_H_ +#define CONTEXT_SWITCHER_ASSEMBLY_BINDINGS_H_ + +/** + * Low level bindings to the two assembly functions used to switch context. + * Can be used standalone to build new control structures, but should be handled with care. + * + * Basic usage: + * // Switch control to a new stack. + * continuation_t cont = __cs_enter_context(my_stack, arg_pointer, callback_func, my_stack_limit); + * // Will only be run when 'jumped back' by either returning the callback or an explicit jump. + * // cont will then hold the context of the jump origin (or null if it was a return form a finished callback). + * cont = __switch_context(cont); + * // Re-Enter the context that jumped to us. + */ + +namespace context_switcher { +namespace assembly_bindings { + +using continuation_t = void *; +using stack_pointer_t = char *; +using callback_t = continuation_t (*)(continuation_t, void *); + +extern "C" { +continuation_t __cs_enter_context(stack_pointer_t stack_base, + void *callback_arg, + callback_t callback, + stack_pointer_t stack_limit); +continuation_t __cs_switch_context(continuation_t continuation); +} + +} +} + +#endif //CONTEXT_SWITCHER_ASSEMBLY_BINDINGS_H_ diff --git a/lib/context_switcher/include/context_switcher/context_switcher.h b/lib/context_switcher/include/context_switcher/context_switcher.h new file mode 100644 index 0000000..757deda --- /dev/null +++ b/lib/context_switcher/include/context_switcher/context_switcher.h @@ -0,0 +1,35 @@ +#ifndef CONTEXT_SWITCHER_CONTEXT_SWITCHER_H_ +#define CONTEXT_SWITCHER_CONTEXT_SWITCHER_H_ + +#include +#include +#include +#include + +#include "assembly_bindings.h" +#include "continuation.h" +#include "lambda_capture.h" + +namespace context_switcher { + +template +continuation enter_context(assembly_bindings::stack_pointer_t stack_memory, size_t stack_size, F &&lambda) { + assembly_bindings::stack_pointer_t lambda_memory = stack_memory + stack_size - sizeof(lambda_capture); + auto *captured_lambda = place_lambda_capture(std::forward(lambda), lambda_memory); + + assembly_bindings::stack_pointer_t stack_base = lambda_memory; + assembly_bindings::stack_pointer_t stack_limit = stack_memory; + + assembly_bindings::callback_t callback = lambda_capture_callback>; + return continuation{assembly_bindings::__cs_enter_context(stack_base, captured_lambda, callback, stack_limit)}; +} + +continuation switch_context(continuation &&cont) { + assembly_bindings::continuation_t cont_pointer = cont.consume(); + + return continuation{assembly_bindings::__cs_switch_context(cont_pointer)}; +} + +} + +#endif //CONTEXT_SWITCHER_CONTEXT_SWITCHER_H_ diff --git a/lib/context_switcher/include/context_switcher/continuation.h b/lib/context_switcher/include/context_switcher/continuation.h new file mode 100644 index 0000000..06de42b --- /dev/null +++ b/lib/context_switcher/include/context_switcher/continuation.h @@ -0,0 +1,52 @@ + +#ifndef CONTEXT_SWITCHER_CONTINUATION_H_ +#define CONTEXT_SWITCHER_CONTINUATION_H_ + +#include "assembly_bindings.h" + +namespace context_switcher { + +/** + * One-Shot continuation. Represents the paused state of an execution thread. + * Can be used exactly once to jump back to that state. + * + * Move only to ensure semantics of one time use. + */ +struct continuation { + public: + continuation() : cont_pointer_{nullptr} {}; + explicit continuation(assembly_bindings::continuation_t cont_pointer) : cont_pointer_{cont_pointer} {}; + + // Move-Only Semantics + continuation(const continuation &other) = delete; + continuation(continuation &&other) noexcept { + cont_pointer_ = other.cont_pointer_; + other.cont_pointer_ = nullptr; + } + + continuation &operator=(const continuation &other) = delete; + continuation &operator=(continuation &&other) noexcept { + cont_pointer_ = other.cont_pointer_; + other.cont_pointer_ = nullptr; + + return *this; + } + + // Semantics as 'one time use' + bool valid() const { + return cont_pointer_ != nullptr; + } + + assembly_bindings::continuation_t consume() { + auto tmp = cont_pointer_; + cont_pointer_ = nullptr; + return tmp; + } + + private: + assembly_bindings::continuation_t cont_pointer_; +}; + +} + +#endif //CONTEXT_SWITCHER_CONTINUATION_H_ diff --git a/lib/context_switcher/include/context_switcher/lambda_capture.h b/lib/context_switcher/include/context_switcher/lambda_capture.h new file mode 100644 index 0000000..ad0fad2 --- /dev/null +++ b/lib/context_switcher/include/context_switcher/lambda_capture.h @@ -0,0 +1,52 @@ + +#ifndef CONTEXT_SWITCHER_LAMBDA_CAPTURE_H_ +#define CONTEXT_SWITCHER_LAMBDA_CAPTURE_H_ + +#include +#include +#include + +#include "assembly_bindings.h" +#include "continuation.h" + +/** + * Helpers to more easily use a lambda expression as the code run on a context switch. + * Captures lambdas by placing them in a memory region and offering a conversion form + * the assembly_bindings callback API to the user's lambda. + */ + +namespace context_switcher { + +template +struct lambda_capture { + explicit lambda_capture(F &&lambda) : lambda_{std::forward(lambda)} {} + + assembly_bindings::continuation_t operator()(assembly_bindings::continuation_t continuation_pointer) { + continuation cont = lambda_(continuation{continuation_pointer}); + return cont.consume(); + } + + private: + F lambda_; +}; + +template +assembly_bindings::continuation_t lambda_capture_callback(assembly_bindings::continuation_t continuation_pointer, + void *lambda_capture_param) { + // Perform Call + T *lambda_capture = reinterpret_cast(lambda_capture_param); + auto result_continuation = (*lambda_capture)(continuation_pointer); + + // Free resources and switch to result_continuation (this execution thread is finished with the return) + lambda_capture->~T(); + return result_continuation; +} + +template +static lambda_capture *place_lambda_capture(F &&lambda, char *memory) { + return new(memory) lambda_capture(std::forward(lambda)); +} + +} + +#endif //CONTEXT_SWITCHER_LAMBDA_CAPTURE_H_ diff --git a/lib/context_switcher/src/context_switcher.cpp b/lib/context_switcher/src/context_switcher.cpp new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/context_switcher/src/context_switcher.cpp @@ -0,0 +1 @@ + diff --git a/lib/pls/CMakeLists.txt b/lib/pls/CMakeLists.txt index b1e4333..b22231d 100644 --- a/lib/pls/CMakeLists.txt +++ b/lib/pls/CMakeLists.txt @@ -58,7 +58,7 @@ add_library(pls STATIC include/pls/internal/scheduling/task.h src/internal/scheduling/task.cpp include/pls/internal/scheduling/cont_manager.h include/pls/internal/scheduling/cont.h - include/pls/internal/data_structures/bounded_ws_deque.h include/pls/internal/data_structures/optional.h include/pls/internal/scheduling/memory_block.h include/pls/internal/scheduling/thread_state_static.h src/internal/base/error_handling.cpp include/pls/internal/data_structures/bounded_trading_deque.h) + include/pls/internal/data_structures/bounded_ws_deque.h include/pls/internal/data_structures/optional.h include/pls/internal/scheduling/memory_block.h include/pls/internal/scheduling/thread_state_static.h src/internal/base/error_handling.cpp include/pls/internal/data_structures/bounded_trading_deque.h ../context_switcher/src/context_switcher.cpp) # Add everything in `./include` to be in the include path of this project target_include_directories(pls