diff --git a/.github/matrix.json b/.github/matrix.json index c32dc4d7..883e8af1 100644 --- a/.github/matrix.json +++ b/.github/matrix.json @@ -7,9 +7,9 @@ "os": "ubuntu-22.04" }, { - "name": "Linux Clang 14", + "name": "Linux Clang 15", "compiler": "clang", - "version": "14", + "version": "15", "os": "ubuntu-22.04" }, { diff --git a/CMakeLists.txt b/CMakeLists.txt index 66242e95..f6730a85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ stlab_detect_thread_system(STLAB_DEFAULT_THREAD_SYSTEM) set( STLAB_THREAD_SYSTEM ${STLAB_DEFAULT_THREAD_SYSTEM} CACHE STRING "Thread system to use (win32|pthread|pthread-emscripten|pthread-apple|none)") stlab_detect_task_system(STLAB_DEFAULT_TASK_SYSTEM) -set(STLAB_TASK_SYSTEM ${STLAB_DEFAULT_TASK_SYSTEM} CACHE STRING "Task system to use (portable|libdispatch|windows).") +set(STLAB_TASK_SYSTEM ${STLAB_DEFAULT_TASK_SYSTEM} CACHE STRING "Task system to use (portable|libdispatch|windows|experimental_rust).") stlab_detect_main_executor(STLAB_DEFAULT_MAIN_EXECUTOR) set(STLAB_MAIN_EXECUTOR ${STLAB_DEFAULT_MAIN_EXECUTOR} CACHE STRING "Main executor to use (qt5|qt6|libdispatch|emscripten|none).") @@ -91,6 +91,10 @@ if (STLAB_TASK_SYSTEM STREQUAL "libdispatch") target_link_libraries(stlab INTERFACE libdispatch::libdispatch) endif() +if (STLAB_TASK_SYSTEM STREQUAL "experimental_rust") + add_subdirectory( rustport ) +endif() + if (STLAB_MAIN_EXECUTOR STREQUAL "libdispatch") target_link_libraries(stlab INTERFACE libdispatch::libdispatch) elseif (STLAB_MAIN_EXECUTOR STREQUAL "qt5") @@ -99,6 +103,7 @@ elseif (STLAB_MAIN_EXECUTOR STREQUAL "qt6") target_link_libraries( stlab INTERFACE Qt6::Core ) endif() +message(STATUS "stlab: Use Boost C++17 Shims: ${STLAB_USE_BOOST_CPP17_SHIMS}") message(STATUS "stlab: Disable Coroutines: ${STLAB_DEFAULT_NO_STD_COROUTINES}") message(STATUS "stlab: Thread System: ${STLAB_THREAD_SYSTEM}") message(STATUS "stlab: Task System: ${STLAB_TASK_SYSTEM}") @@ -123,6 +128,10 @@ if ( BUILD_TESTING ) stlab::development stlab::stlab ) + if (STLAB_TASK_SYSTEM STREQUAL "experimental_rust") + target_link_libraries( testing INTERFACE RustyDefaultExecutor ) + endif () + # # Linking to the Boost unit test framework requires an additional # preprocessor definition when the unit test compiled resources are diff --git a/cmake/StlabUtil.cmake b/cmake/StlabUtil.cmake index 35fdb665..13c1df53 100644 --- a/cmake/StlabUtil.cmake +++ b/cmake/StlabUtil.cmake @@ -166,6 +166,8 @@ function( stlab_generate_config_file ) set( STLAB_TASK_SYSTEM_EMSCRIPTEN TRUE ) elseif (STLAB_TASK_SYSTEM STREQUAL "windows") set( STLAB_TASK_SYSTEM_WINDOWS TRUE ) + elseif (STLAB_TASK_SYSTEM STREQUAL "experimental_rust") + set( STLAB_TASK_SYSTEM_EXPERIMENTAL_RUST TRUE ) endif() if (STLAB_MAIN_EXECUTOR STREQUAL "libdispatch") diff --git a/rustport/.gitignore b/rustport/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/rustport/.gitignore @@ -0,0 +1 @@ +target diff --git a/rustport/CMakeLists.txt b/rustport/CMakeLists.txt new file mode 100644 index 00000000..cba82edc --- /dev/null +++ b/rustport/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.25) + +include(FetchContent) + +FetchContent_Declare( + Corrosion + GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git + GIT_TAG master # Needed for experimental feature `corrosion_experimental_cbindgen`. +) + +# Set any global configuration variables such as `Rust_TOOLCHAIN` before this line! + +FetchContent_MakeAvailable(Corrosion) + +corrosion_import_crate(MANIFEST_PATH ./Cargo.toml) + +corrosion_experimental_cbindgen( + TARGET default_executor + HEADER_NAME "bindings.h" +) + +add_library(RustyDefaultExecutor INTERFACE) + +target_include_directories(RustyDefaultExecutor + # PRIVATE + # # where the library itself will look for its internal headers + # ${CMAKE_CURRENT_SOURCE_DIR}/src + INTERFACE + # where top-level project will look for the library's public headers + $ +) + +target_link_libraries(RustyDefaultExecutor INTERFACE default_executor) diff --git a/rustport/Cargo.lock b/rustport/Cargo.lock new file mode 100644 index 00000000..2a4c4733 --- /dev/null +++ b/rustport/Cargo.lock @@ -0,0 +1,483 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cbindgen" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" +dependencies = [ + "clap", + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "default_executor" +version = "0.1.0" +dependencies = [ + "cbindgen", + "once_cell", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "termcolor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/rustport/Cargo.toml b/rustport/Cargo.toml new file mode 100644 index 00000000..4caf0fbd --- /dev/null +++ b/rustport/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "default_executor" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +once_cell = "1.18.0" + +[build-dependencies] +cbindgen = "0.24.0" + diff --git a/rustport/cppshim/include/bindings.hpp b/rustport/cppshim/include/bindings.hpp new file mode 100644 index 00000000..98f8358c --- /dev/null +++ b/rustport/cppshim/include/bindings.hpp @@ -0,0 +1,40 @@ +#include "bindings.h" + +#include +#include +#include + +namespace rust { + +/// @brief Asynchronously invoke f on the rust-backed executor. +/// @tparam F function object type +/// @param f a function object. +/// @return the result of calling `execute`. +template ::value>> +auto enqueue(F f) { + return execute(new F(std::move(f)), [](void* f_) { + auto f = static_cast(f_); + try { + (*f)(); + } catch (...) {} + delete f; + }); +} + +/// @brief Asynchronously invoke `f` on the rust-backed executor with priority `p`. +/// @tparam F function object type +/// @param f a function object. +/// @param priority +/// @return the value returned by `execute_priority`. +template ::value> > +auto enqueue_priority(F f, Priority p) { + return execute_priority(new F(std::move(f)), [](void* f_) { + auto f = static_cast(f_); + try { + (*f)(); + } catch (...) {} + delete f; + }, p); +} + +} diff --git a/rustport/src/lib.rs b/rustport/src/lib.rs new file mode 100644 index 00000000..ee2e377a --- /dev/null +++ b/rustport/src/lib.rs @@ -0,0 +1,56 @@ +use once_cell::sync::Lazy; +use std::{ffi::c_void, sync::Mutex}; + +use stlab::{Priority, PriorityTaskSystem}; +mod stlab; + +/// A static instance of the task system which is invoked through the `execute` functions below. +static TASK_SYSTEM: Lazy> = + Lazy::new(|| Mutex::new(PriorityTaskSystem::new())); + + +/// A function pointer paired with a context, akin to a C++ lambda and its captures. +/// +/// "Threadsafe" is not a guarantee, it is a requirement. These pointers are assumed to be able to +/// be sent to another thread, and therefore must not rely on thread-local state. +struct ThreadsafeCFnWrapper { + context: *mut c_void, + fn_ptr: extern "C" fn(*mut c_void), +} + +impl ThreadsafeCFnWrapper { + pub(crate) fn new(context: *mut c_void, fn_ptr: extern "C" fn(*mut c_void)) -> Self { + Self { context, fn_ptr } + } + + // Note: there is no way in stable rust to make a struct invocable with () syntax. + pub(crate) fn call(&self) { + (self.fn_ptr)(self.context) + } +} + +/// `ThreadsafeCFnWrapper` may not rely on thread-local state. +unsafe impl Send for ThreadsafeCFnWrapper {} + +/// Enqueues a the execution of `f(context)` on the PriorityTaskSystem. +/// +/// Precondition: Neither `context` nor `fn_ptr` may rely on thread-local state. +#[no_mangle] +pub extern "C" fn execute(context: *mut c_void, fn_ptr: extern "C" fn(*mut c_void)) -> i32 { + execute_priority(context, fn_ptr, Priority::Default); + 0 +} + +/// Enqueues a the execution of `f(context)` on the PriorityTaskSystem at the given `priority`. +/// +/// Precondition: Neither `context` nor `fn_ptr` may rely on thread-local state. +#[no_mangle] +pub extern "C" fn execute_priority( + context: *mut c_void, + fn_ptr: extern "C" fn(*mut c_void), + p: Priority, +) -> i32 { + let wrap = ThreadsafeCFnWrapper::new(context, fn_ptr); + TASK_SYSTEM.lock().unwrap().execute(move || wrap.call(), p); + 0 +} diff --git a/rustport/src/stlab/coarse_priority_queue.rs b/rustport/src/stlab/coarse_priority_queue.rs new file mode 100644 index 00000000..81327fcc --- /dev/null +++ b/rustport/src/stlab/coarse_priority_queue.rs @@ -0,0 +1,120 @@ +use std::cmp::Ordering; + +/// A priority queue with three priorities. Elements with equal priorities are popped in FIFO order. +pub struct CoarsePriorityQueue { + inner: std::collections::BinaryHeap>, + count: usize, +} + +impl CoarsePriorityQueue { + /// Creates a new, empty `CoarsePriorityQueue`. + pub fn new() -> Self { + Self { + inner: std::collections::BinaryHeap::new(), + count: 0, + } + } + + /// Checks if the queue is empty. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Removes the greatest item from the queue at returns it, or None if it is empty. + pub fn pop(&mut self) -> Option { + self.inner + .pop() + .and_then(|prioritized| Some(prioritized.take_inner())) + } + + /// Pushes an item onto the queue at the given `priority`. + pub fn push(&mut self, item: T, priority: Priority) { + self.inner + .push(Prioritized::new(item, priority, self.count)); + self.count += 1; + } +} + +/// The three priorities used by `CoarsePriorityQueue`. +#[derive(Eq, PartialEq, Copy, Clone)] +#[repr(C)] +pub enum Priority { + Low, + Default, + High, +} + +/// Use of `Priority` and `Prioritized` requires the client maintain a nondecreasing count which +/// is supplied to the constructor of `Prioritized`. That count is bitwise-OR'd with the +/// result of `highbit_mask` for a given `Priority`. The resulting `usize` is used for ordering and +/// equality of a `Prioritized`. The purpose of this is to implement FIFO ordering (with coarse +/// priorities) with a single BinaryHeap. +/// +/// Note: This implies the value of the count must not exceed +/// 0b001111111111111111111111111111111111111111111111111111111111111, or 2,305,843,009,213,693,951, +/// but this is considered unlikely enough to not be exposed in the public documentation for +/// CoarsePriorityQueue. +impl Priority { + const fn highbit_mask(&self) -> usize { + match self { + Priority::Low => 0 << usize::BITS - 2, + Priority::Default => 1 << usize::BITS - 2, + Priority::High => 2 << usize::BITS - 2, + } + } + + /// Precondition: count < 2,305,843,009,213,693,951 + fn merge_priority_count(&self, count: usize) -> usize { + self.highbit_mask() | count + } +} + +impl PartialOrd for Priority { + fn partial_cmp(&self, other: &Self) -> Option { + self.highbit_mask().partial_cmp(&other.highbit_mask()) + } +} + +/// Pairs an instance of `T` with a `Priority`. +/// Equality and ordering of a Prioritized only considers `priority`, and disregards `inner`. +struct Prioritized { + inner: T, + priority: usize, +} + +impl Prioritized { + /// Precondition: count must be less than 2,305,843,009,213,693,951. + pub fn new(inner: T, priority: Priority, count: usize) -> Self { + Prioritized { + inner, + priority: priority.merge_priority_count(count), + } + } + + pub fn take_inner(self) -> T { + self.inner + } +} + +impl PartialEq for Prioritized { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.priority == other.priority + } +} + +impl Eq for Prioritized {} + +impl PartialOrd for Prioritized { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.priority.partial_cmp(&other.priority) + } +} + +impl Ord for Prioritized { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.priority.cmp(&other.priority) + } +} diff --git a/rustport/src/stlab/drop_join_thread_pool.rs b/rustport/src/stlab/drop_join_thread_pool.rs new file mode 100644 index 00000000..cf461e29 --- /dev/null +++ b/rustport/src/stlab/drop_join_thread_pool.rs @@ -0,0 +1,106 @@ +use std::{io::Write, thread::JoinHandle}; + +/// A thread pool which joins all spawned threads when dropped. +/// +/// This pool also holds a pointer to a heap-allocated object which is shared by all spawned +/// threads. As such, it must be Send + Sync, and 'static (having no non-static references). When +/// tasks in this pool are spawned, they are passed an immutable reference to said object (there is +/// no way to acquire a mutable reference). +/// +/// If a thread panics, the panic will be propagated while dropping this pool. +/// +/// Note: it remains to be seen if it is possible (or useful) to loosen the 'static requirement here +/// to instead allow any lifetimes which do not outlive this pool. Early attempts at this polluted +/// the API with explicit lifetimes. See +/// https://users.rust-lang.org/t/access-to-implicit-lifetime-of-containing-object-aka-self-lifetime/18917 +/// for a relevant disucssion. +pub struct DropJoinThreadPool { + threads: Vec>, + // SAFETY: We store this pointer as a `*mut` so it can be dropped via Box::from_raw, but + // we only ever hand out immutable references to the pointee. + data: *mut T, +} + +impl Drop for DropJoinThreadPool { + /// Join all spawned threads, and drop `data` manually. + /// + /// If a thread in the pool paniced, that panic will propagate here. + fn drop(&mut self) { + for thread in std::mem::take(&mut self.threads) { + match thread.join() { + Ok(..) => continue, + Err(e) => std::panic::resume_unwind(e) + } + } + + // SAFETY: We only call from_raw once, in this `drop`. We do not permit copies of this + // object, which would present the risk of a double-free on the copied pointer. + unsafe { drop(Box::from_raw(self.data)) }; + } +} + +impl DropJoinThreadPool { + /// Creates a new `DropJoinThreadPool` with a maximum thread capacity of `thread_limit`, moving + /// `data` onto the heap. + pub fn new(thread_limit: usize, data: T) -> Self { + Self { + threads: Vec::>::with_capacity(thread_limit), + data: Box::into_raw(Box::new(data)), + } + } + + /// Performs an operation with an immutable reference to this pool's `data` on the current + /// thread. + pub fn execute_immediately Return>(&self, f: F) -> Return { + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + f(data) + } + + /// Spawns at most `n` threads with the task `f` until this pool's thread capacity is reached. + /// + /// If spawning all `n` threads would exceed this pool's thread capacity, a message is logged to + /// stderr. + pub fn spawn_n(&mut self, f: F, mut n: usize) { + if self.threads.len() + n > self.threads.capacity() { + Self::log_err(b"stlab: Unable to spawn all threads; capacity reached."); + n = self.threads.capacity() - self.threads.len(); + } + + self.threads.extend((0..n).map(|i| { + let f = f.clone(); + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + std::thread::spawn(move || { + f(i, data); + }) + })); + } + + /// Spawns a single thread with the task `f`, unless doing so would exceed this pool's thread + /// capacity. + /// + /// If the thread is not spawned due to inadequate space, a message is logged to stderr. + pub fn spawn(&mut self, f: F) { + let i = self.threads.len(); + + if i == self.threads.capacity() { + Self::log_err(b"stlab: Unable to spawn thread; capacity reached."); + return; + } + + self.threads.push({ + // SAFETY: We only hand out immutable references to this data. + let data: &T = unsafe { &*self.data }; + std::thread::spawn(move || { + f(i, data); + }) + }); + } + + /// Write `buf` to stderr, discarding the ioresult. + fn log_err(buf: &[u8]) { + let mut stderr = std::io::stderr().lock(); + let _ = stderr.write_all(buf); + } +} diff --git a/rustport/src/stlab/mod.rs b/rustport/src/stlab/mod.rs new file mode 100644 index 00000000..b868d1b7 --- /dev/null +++ b/rustport/src/stlab/mod.rs @@ -0,0 +1,179 @@ +use std::cmp::max; +use std::num::NonZeroUsize; +use std::sync::atomic::{AtomicUsize, Ordering as MemoryOrdering}; +use std::sync::Arc; + +mod coarse_priority_queue; +pub use coarse_priority_queue::Priority; + +mod drop_join_thread_pool; +use drop_join_thread_pool::DropJoinThreadPool; + +mod waiter; +pub use waiter::Waiter; + +pub mod notification_queue; +use notification_queue::NotificationQueue; + +/// A type-erased, heap-allocated function object. +pub type Task = Box () + Send>; + +/// A portable work-stealing task scheduler with three priorities. +/// +/// This scheduler spins up a number of threads corresponding to the amount of parallelism available +/// on the target platform, namely, std::thread::available_parallelism() - 1. Each thread is +/// assigned a threadsafe priority queue. To reduce contention on push and pop operations, a thread +/// will first attempt to acquire the lock for its own queue without blocking. +/// If that fails, it will attempt the same non-blocking push/pop for each other priority queue in +/// the scheduler. Finally, if each of those attempts also fail, the thread will attempt a blocking +/// push/pop on its own priority queue. +/// +/// The `add_thread` API is intended to mitigate the possibility of deadlock by spinning up a new +/// worker thread that non-blockingly polls all of the system's priority queues, and then sleeps +/// until `wake()` is called. +pub struct PriorityTaskSystem { + pool: DropJoinThreadPool>>, + waiters: Arc>, + index: AtomicUsize, + available_parallelism: usize, +} + +impl Drop for PriorityTaskSystem { + fn drop(&mut self) { + self.pool.execute_immediately(|queues| { + for queue in queues.iter() { + queue.done() + } + }); + + for waiter in self.waiters.iter() { + waiter.done() + } + } +} + +impl PriorityTaskSystem { + /// Creates a new PriorityTaskSystem. + pub fn new() -> Self { + // SAFETY: We know 1 is not 0. + let nonzero_available_parallelism = std::thread::available_parallelism() + .unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); + let available_parallelism = max(usize::from(nonzero_available_parallelism), 2) - 1; + let thread_limit = max(9, available_parallelism * 4 + 1); + let queues = (0..available_parallelism) + .map(|_| NotificationQueue::default()) + .collect(); + + let mut pool = DropJoinThreadPool::new(thread_limit, queues); + + pool.spawn_n( + move |i, queues| loop { + let mut task = Self::try_pop(queues, i, available_parallelism); + + if task.is_none() { + let done: bool; + (done, task) = queues.get(i).unwrap().pop(); + if done { + break; + } + } + + if task.is_some() { + task.unwrap()(); + } + }, + available_parallelism, + ); + + Self { + pool, + waiters: Arc::new( + (0..(thread_limit - available_parallelism)) + .map(|_| Waiter::default()) + .collect(), + ), + index: AtomicUsize::new(0), + available_parallelism, + } + } + + /// Push `f` to the first queue in `queues` whose mutex is not under contention. + /// If no such queue is found after a single pass, blockingly push `f` to one queue. + pub fn execute(&self, f: F, p: Priority) + where + F: FnOnce() -> () + Send + 'static, + { + self.execute_task(Box::new(f), p) + } + + /// Push `task` to the first queue in `queues` whose mutex is not under contention. + /// If no such queue is found after a single pass, blockingly push `task` to one queue. + pub fn execute_task(&self, task: Task, priority: Priority) { + self.pool.execute_immediately(|queues| { + let mut task: Option = Some(task); + let i = self.index.fetch_add(1, MemoryOrdering::SeqCst); + let n = self.available_parallelism; + + // Attempt to push to each queue without blocking. + for i in (i..i + n).map(|i| i % n) { + task = queues.get(i).unwrap().try_push(task.unwrap(), priority); + if task.is_none() { + return; + } // An empty return means push was successful. + } + + // Otherwise, attempt to blockingly push to one queue. + queues.get(i % n).unwrap().push(task.unwrap(), priority); + }); + } + + /// Add a work-stealing thread to the scheduler to mitigate deadlock. + pub fn add_thread(&mut self) { + let waiters = self.waiters.clone(); + let n = self.available_parallelism; + self.pool.spawn(move |i, queues| { + loop { + if let Some(task) = Self::try_pop(queues, i, n) { + task(); + continue; + } + + // Note: The following means multiple threads may wait on a single `Waiter`. + if waiters[i - n].wait() { + break; + } + } + }); + } + + // Returns true if any thread was woken. + pub fn wake(&self) -> bool { + let any_queue_woken = self + .pool + .execute_immediately(|queues| return queues.iter().any(|queue| queue.wake())); + + if any_queue_woken { + true + } else { + self.waiters.iter().any(|waiter| waiter.wake()) + } + } + + /// Attempt to non-blockingly pop a task from each queue in the system, starting at index + /// `starting_at`. + fn try_pop( + queues: &Vec>, + starting_at: usize, + modulo: usize, + ) -> Option { + for i in (starting_at..starting_at + modulo).map(|i| i % modulo) { + match queues.get(i).unwrap().try_pop() { + Some(t) => return Some(t), + None => continue, + }; + } + return None; + } +} + +unsafe impl Send for PriorityTaskSystem {} diff --git a/rustport/src/stlab/notification_queue.rs b/rustport/src/stlab/notification_queue.rs new file mode 100644 index 00000000..2773f1ae --- /dev/null +++ b/rustport/src/stlab/notification_queue.rs @@ -0,0 +1,107 @@ +use crate::stlab::coarse_priority_queue::{CoarsePriorityQueue, Priority}; + +/// A threadsafe priority queue. +pub struct NotificationQueue { + protected: std::sync::Mutex>, + ready: std::sync::Condvar, +} + +impl std::default::Default for NotificationQueue { + fn default() -> Self { + Self { + protected: std::sync::Mutex::new(NotificationQueueProtectedData::::default()), + ready: std::sync::Condvar::new(), + } + } +} + +impl NotificationQueue { + /// Try to pop from the queue without blocking. + /// Returns `None` if our mutex is already locked or if the queue is empty. + pub fn try_pop(&self) -> Option { + if let Ok(ref mut this) = self.protected.try_lock() { + return this.queue.pop(); + } + return None; + } + + /// If waiting in `pop()`, wakes and returns true. Otherwise returns false. + pub fn wake(&self) -> bool { + if let Ok(ref mut this) = self.protected.try_lock() { + if !this.waiting { + return false; + } + this.waiting = false; // triggers wake + } + self.ready.notify_one(); + return true; + } + + /// Pop from the queue, suspending the current thread until an element is available. + /// The returned `bool` indicates if this object is `done()`. + pub fn pop(&self) -> (bool, Option) { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.waiting = true; + while !this.queue.is_empty() && !this.done && this.waiting { + this = self.ready.wait(this).expect("the mutex is not poisoned"); + } + this.waiting = false; + if this.queue.is_empty() { + return (this.done, None); + } + return (false, this.queue.pop()); + } + + /// Mark this object for teardown, and wake any thread awaiting an available element in `pop()`. + pub fn done(&self) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.done = true + } + self.ready.notify_one(); + } + + /// Try to push `element` to the queue without blocking, returning `element` if our mutex is already locked. + /// If the push succeeds, wake a thread which may be awaiting an element in `pop()`. + pub fn try_push(&self, element: T, priority: Priority) -> Option { + if let Ok(ref mut this) = self.protected.try_lock() { + this.queue.push(element, priority); + } else { + return Some(element); + } + + // We successfully locked the mutex, did our push, and released the lock. + self.ready.notify_one(); + return None; + } + + /// Push `element` to the queue, blocking if our mutex is already locked. + /// When the push succeeds, wake a thread which may be awaiting an element in `pop()`. + pub fn push(&self, element: T, priority: Priority) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.queue.push(element, priority); + } + self.ready.notify_one(); + } +} + +unsafe impl Send for NotificationQueue {} +unsafe impl Sync for NotificationQueue {} + +/// The fields of `NotificationQueue` which must be protected by a `Mutex`. +struct NotificationQueueProtectedData { + queue: CoarsePriorityQueue, + done: bool, + waiting: bool, +} + +impl std::default::Default for NotificationQueueProtectedData { + fn default() -> Self { + Self { + queue: CoarsePriorityQueue::new(), + done: false, + waiting: false, + } + } +} diff --git a/rustport/src/stlab/waiter.rs b/rustport/src/stlab/waiter.rs new file mode 100644 index 00000000..9d6c6b06 --- /dev/null +++ b/rustport/src/stlab/waiter.rs @@ -0,0 +1,73 @@ +use std::sync::Mutex; + +// REVISIT: It may be wise to reimplement this in terms of std::thread::park. + +/// A utility for suspending a thread using a condition variable. +pub struct Waiter { + protected: Mutex, + ready: std::sync::Condvar, +} + +impl Default for Waiter { + fn default() -> Self { + Waiter::new() + } +} + +impl Waiter { + /// Constructs a new Waiter, with `waiting` and `done` set to `false`. + pub fn new() -> Self { + Self { + protected: Mutex::new(WaiterProtectedData { + waiting: false, + done: false, + }), + ready: std::sync::Condvar::new(), + } + } + + /// Sets `done` to `true`, and notifies one waiter of our condition variable. + pub fn done(&self) { + { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.done = true; + } + self.ready.notify_one(); + } + + /// Sets waiting to `false`. If waiting was `true`, wake one waiter and return `true`. Otherwise, return `false`. + /// If `try_lock` fails, return `false`. + pub fn wake(&self) -> bool { + if let Ok(ref mut this) = self.protected.try_lock() { + if !this.waiting { + return false; + } + this.waiting = false; + } else { + return false; + } + self.ready.notify_one(); + return true; + } + + /// Block this thread until `wake()` or `done()` is called. + /// Returns `true` if `done()` has been called, otherwise `false`. + pub fn wait(&self) -> bool { + let mut this = self.protected.lock().expect("the mutex is not poisoned"); + this.waiting = true; + while this.waiting && !this.done { + this = self.ready.wait(this).expect("the mutex is not poisoned"); + } + this.waiting = false; + return this.done; + } +} + +unsafe impl Send for Waiter {} +unsafe impl Sync for Waiter {} + +/// The fields of `Waiter` which must be protected by a `Mutex`. +struct WaiterProtectedData { + waiting: bool, + done: bool, +} diff --git a/stlab/concurrency/default_executor.hpp b/stlab/concurrency/default_executor.hpp index 506aa471..5b3f9080 100644 --- a/stlab/concurrency/default_executor.hpp +++ b/stlab/concurrency/default_executor.hpp @@ -33,6 +33,8 @@ #include #include #include +#elif STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) +#include "bindings.hpp" #endif /**************************************************************************************************/ @@ -485,6 +487,36 @@ struct executor_type { /**************************************************************************************************/ +#if STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) + +inline auto bridge_priority(executor_priority p) -> Priority { + switch (p) { + case executor_priority::high: return Priority::High; + case executor_priority::medium: return Priority::Default; + case executor_priority::low: return Priority::Low; + default: return Priority::Default; + } +} + +/// @brief A thin invokable wrapper around `enqueue_priority`. +/// @tparam Priority the priority at which all given function objects will be enqueued. +template +struct executor_type { + using result_type = void; + + /// @brief Enqueues the given task on the default_executor with this object's Priority value. + /// @tparam F function object type + /// @param f function object + template + void operator()(F&& f) const { + rust::enqueue_priority(std::forward(f), bridge_priority(Priority)); + } +}; + +#endif // STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) + +/**************************************************************************************************/ + } // namespace detail /**************************************************************************************************/ @@ -493,6 +525,7 @@ constexpr auto low_executor = detail::executor_type{}; constexpr auto high_executor = detail::executor_type{}; + /**************************************************************************************************/ } // namespace v1 diff --git a/stlab/concurrency/system_timer.hpp b/stlab/concurrency/system_timer.hpp index 1c85da9c..8c320f35 100755 --- a/stlab/concurrency/system_timer.hpp +++ b/stlab/concurrency/system_timer.hpp @@ -22,7 +22,7 @@ #elif STLAB_TASK_SYSTEM(WINDOWS) #include #include -#elif STLAB_TASK_SYSTEM(PORTABLE) +#elif STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) #include #include #include @@ -161,7 +161,7 @@ class system_timer { /**************************************************************************************************/ -#elif STLAB_TASK_SYSTEM(PORTABLE) +#elif STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) class system_timer { using element_t = std::pair>; @@ -241,7 +241,7 @@ class system_timer { /**************************************************************************************************/ -#if STLAB_TASK_SYSTEM(WINDOWS) || STLAB_TASK_SYSTEM(PORTABLE) +#if STLAB_TASK_SYSTEM(WINDOWS) || STLAB_TASK_SYSTEM(PORTABLE) || STLAB_TASK_SYSTEM(EXPERIMENTAL_RUST) struct system_timer_type { using result_type = void; diff --git a/stlab/config.hpp.in b/stlab/config.hpp.in index 5b4b85a3..641b5613 100644 --- a/stlab/config.hpp.in +++ b/stlab/config.hpp.in @@ -23,6 +23,7 @@ #cmakedefine01 STLAB_TASK_SYSTEM_LIBDISPATCH() #cmakedefine01 STLAB_TASK_SYSTEM_EMSCRIPTEN() #cmakedefine01 STLAB_TASK_SYSTEM_WINDOWS() +#cmakedefine01 STLAB_TASK_SYSTEM_EXPERIMENTAL_RUST() #define STLAB_MAIN_EXECUTOR(X) (STLAB_MAIN_EXECUTOR_##X()) #cmakedefine01 STLAB_MAIN_EXECUTOR_LIBDISPATCH()