├── tests ├── large │ ├── run.sh │ ├── libcosmo_plugin │ ├── CMakeLists.txt │ └── main.cpp ├── stress │ ├── run.sh │ ├── libcosmo_plugin │ ├── CMakeLists.txt │ └── main.cpp ├── bidirectional │ ├── run.sh │ ├── libcosmo_plugin │ ├── CMakeLists.txt │ └── main.cpp ├── signed-unsigned │ ├── run.sh │ ├── libcosmo_plugin │ ├── CMakeLists.txt │ └── main.cpp └── run.sh ├── .github ├── cosmocc_version.txt └── workflows │ ├── test.yml │ └── test_single.yml ├── .gitmodules ├── .gitignore ├── .vscode └── c_cpp_properties.json ├── LICENSE ├── include ├── LockingQueue.hpp └── cosmo_plugin.hpp ├── CMakeLists.txt ├── README.md └── src └── cosmo_plugin.cpp /tests/large/run.sh: -------------------------------------------------------------------------------- 1 | ../run.sh -------------------------------------------------------------------------------- /tests/stress/run.sh: -------------------------------------------------------------------------------- 1 | ../run.sh -------------------------------------------------------------------------------- /.github/cosmocc_version.txt: -------------------------------------------------------------------------------- 1 | 4.0.2 -------------------------------------------------------------------------------- /tests/large/libcosmo_plugin: -------------------------------------------------------------------------------- 1 | ../../ -------------------------------------------------------------------------------- /tests/bidirectional/run.sh: -------------------------------------------------------------------------------- 1 | ../run.sh -------------------------------------------------------------------------------- /tests/signed-unsigned/run.sh: -------------------------------------------------------------------------------- 1 | ../run.sh -------------------------------------------------------------------------------- /tests/stress/libcosmo_plugin: -------------------------------------------------------------------------------- 1 | ../../ -------------------------------------------------------------------------------- /tests/bidirectional/libcosmo_plugin: -------------------------------------------------------------------------------- 1 | ../../ -------------------------------------------------------------------------------- /tests/signed-unsigned/libcosmo_plugin: -------------------------------------------------------------------------------- 1 | ../../ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/reflect-cpp"] 2 | path = third_party/reflect-cpp 3 | url = https://github.com/getml/reflect-cpp.git 4 | [submodule "third_party/msgpack-c"] 5 | path = third_party/msgpack-c 6 | url = https://github.com/msgpack/msgpack-c.git 7 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | rm -rf build-* *.elf *.com *.dbg *.so 6 | 7 | # Build with cosmoc++ 8 | mkdir -p build-cosmo 9 | cd build-cosmo 10 | CC=cosmocc CXX=cosmoc++ cmake .. -DBUILD_EXE=ON 11 | make -j 12 | cd .. 13 | 14 | # Build with g++ 15 | mkdir -p build-gcc 16 | cd build-gcc 17 | CC=cc CXX=c++ cmake .. 18 | make -j 19 | cd .. 20 | 21 | cp build-cosmo/cosmo.com . 22 | cp build-gcc/libnative.so . 23 | ./cosmo.com libnative.so -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | *.com 34 | *.dbg 35 | 36 | # cmake 37 | build* 38 | 39 | # gdb 40 | .gdb_history 41 | 42 | # Cosmopolitan 43 | cosmo -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "${workspaceFolder}/include", 8 | "${workspaceFolder}/third_party/reflect-cpp/include" 9 | ], 10 | "defines": [ 11 | "__COSMOPOLITAN__" 12 | ], 13 | "compilerPath": "/usr/bin/clang", 14 | "cStandard": "c17", 15 | "cppStandard": "c++20", 16 | "intelliSenseMode": "linux-clang-x64" 17 | } 18 | ], 19 | "version": 4 20 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and run all tests 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: ["main"] 8 | 9 | jobs: 10 | test: 11 | name: Test ${{ matrix.test }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | test: 16 | - bidirectional 17 | - stress 18 | - large 19 | - signed-unsigned 20 | encoding: 21 | - msgpack 22 | - json 23 | uses: ./.github/workflows/test_single.yml 24 | with: 25 | test: ${{ matrix.test }} 26 | encoding: ${{ matrix.encoding }} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Brett Jia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/large/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | project(test) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_BUILD_TYPE Debug) 7 | 8 | add_subdirectory(${PROJECT_SOURCE_DIR}/libcosmo_plugin) 9 | 10 | find_package (Threads REQUIRED) 11 | 12 | if(BUILD_EXE) 13 | if(NOT DEFINED BINARY_NAME) 14 | set(BINARY_NAME "cosmo.com") 15 | endif() 16 | 17 | add_executable(${BINARY_NAME} 18 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 19 | ${LIBCOSMO_PLUGIN_SOURCES} 20 | ) 21 | else() 22 | if(NOT DEFINED BINARY_NAME) 23 | set(BINARY_NAME "native") 24 | endif() 25 | 26 | add_library(${BINARY_NAME} SHARED 27 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 28 | ${LIBCOSMO_PLUGIN_SOURCES} 29 | ) 30 | endif() 31 | 32 | target_include_directories(${BINARY_NAME} PRIVATE 33 | ${LIBCOSMO_PLUGIN_INCLUDE_DIRS} 34 | ) 35 | 36 | if(USE_JSON_ENCODING) 37 | target_compile_definitions(${BINARY_NAME} PRIVATE USE_JSON_ENCODING) 38 | endif() 39 | 40 | if(WIN32) 41 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads wsock32 ws2_32) 42 | else() 43 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads) 44 | endif() -------------------------------------------------------------------------------- /tests/stress/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | project(test) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_BUILD_TYPE Debug) 7 | 8 | add_subdirectory(${PROJECT_SOURCE_DIR}/libcosmo_plugin) 9 | 10 | find_package (Threads REQUIRED) 11 | 12 | if(BUILD_EXE) 13 | if(NOT DEFINED BINARY_NAME) 14 | set(BINARY_NAME "cosmo.com") 15 | endif() 16 | 17 | add_executable(${BINARY_NAME} 18 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 19 | ${LIBCOSMO_PLUGIN_SOURCES} 20 | ) 21 | else() 22 | if(NOT DEFINED BINARY_NAME) 23 | set(BINARY_NAME "native") 24 | endif() 25 | 26 | add_library(${BINARY_NAME} SHARED 27 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 28 | ${LIBCOSMO_PLUGIN_SOURCES} 29 | ) 30 | endif() 31 | 32 | target_include_directories(${BINARY_NAME} PRIVATE 33 | ${LIBCOSMO_PLUGIN_INCLUDE_DIRS} 34 | ) 35 | 36 | if(USE_JSON_ENCODING) 37 | target_compile_definitions(${BINARY_NAME} PRIVATE USE_JSON_ENCODING) 38 | endif() 39 | 40 | if(WIN32) 41 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads wsock32 ws2_32) 42 | else() 43 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads) 44 | endif() -------------------------------------------------------------------------------- /tests/bidirectional/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | project(test) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_BUILD_TYPE Debug) 7 | 8 | add_subdirectory(${PROJECT_SOURCE_DIR}/libcosmo_plugin) 9 | 10 | find_package (Threads REQUIRED) 11 | 12 | if(BUILD_EXE) 13 | if(NOT DEFINED BINARY_NAME) 14 | set(BINARY_NAME "cosmo.com") 15 | endif() 16 | 17 | add_executable(${BINARY_NAME} 18 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 19 | ${LIBCOSMO_PLUGIN_SOURCES} 20 | ) 21 | else() 22 | if(NOT DEFINED BINARY_NAME) 23 | set(BINARY_NAME "native") 24 | endif() 25 | 26 | add_library(${BINARY_NAME} SHARED 27 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 28 | ${LIBCOSMO_PLUGIN_SOURCES} 29 | ) 30 | endif() 31 | 32 | target_include_directories(${BINARY_NAME} PRIVATE 33 | ${LIBCOSMO_PLUGIN_INCLUDE_DIRS} 34 | ) 35 | 36 | if(USE_JSON_ENCODING) 37 | target_compile_definitions(${BINARY_NAME} PRIVATE USE_JSON_ENCODING) 38 | endif() 39 | 40 | if(WIN32) 41 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads wsock32 ws2_32) 42 | else() 43 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads) 44 | endif() -------------------------------------------------------------------------------- /tests/signed-unsigned/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | project(test) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_BUILD_TYPE Debug) 7 | 8 | add_subdirectory(${PROJECT_SOURCE_DIR}/libcosmo_plugin) 9 | 10 | find_package (Threads REQUIRED) 11 | 12 | if(BUILD_EXE) 13 | if(NOT DEFINED BINARY_NAME) 14 | set(BINARY_NAME "cosmo.com") 15 | endif() 16 | 17 | add_executable(${BINARY_NAME} 18 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 19 | ${LIBCOSMO_PLUGIN_SOURCES} 20 | ) 21 | else() 22 | if(NOT DEFINED BINARY_NAME) 23 | set(BINARY_NAME "native") 24 | endif() 25 | 26 | add_library(${BINARY_NAME} SHARED 27 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 28 | ${LIBCOSMO_PLUGIN_SOURCES} 29 | ) 30 | endif() 31 | 32 | target_include_directories(${BINARY_NAME} PRIVATE 33 | ${LIBCOSMO_PLUGIN_INCLUDE_DIRS} 34 | ) 35 | 36 | if(USE_JSON_ENCODING) 37 | target_compile_definitions(${BINARY_NAME} PRIVATE USE_JSON_ENCODING) 38 | endif() 39 | 40 | if(WIN32) 41 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads wsock32 ws2_32) 42 | else() 43 | target_link_libraries(${BINARY_NAME} PUBLIC Threads::Threads) 44 | endif() -------------------------------------------------------------------------------- /include/LockingQueue.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // https://gist.github.com/thelinked/6997598 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | template 10 | class LockingQueue 11 | { 12 | public: 13 | void push(T const& _data) 14 | { 15 | { 16 | std::lock_guard lock(guard); 17 | queue.push(_data); 18 | } 19 | signal.notify_one(); 20 | } 21 | 22 | bool empty() const 23 | { 24 | std::lock_guard lock(guard); 25 | return queue.empty(); 26 | } 27 | 28 | bool tryPop(T& _value) 29 | { 30 | std::lock_guard lock(guard); 31 | if (queue.empty()) 32 | { 33 | return false; 34 | } 35 | 36 | _value = queue.front(); 37 | queue.pop(); 38 | return true; 39 | } 40 | 41 | void waitAndPop(T& _value) 42 | { 43 | std::unique_lock lock(guard); 44 | while (queue.empty()) 45 | { 46 | signal.wait(lock); 47 | } 48 | 49 | _value = queue.front(); 50 | queue.pop(); 51 | } 52 | 53 | bool tryWaitAndPop(T& _value, int _milli) 54 | { 55 | std::unique_lock lock(guard); 56 | 57 | while (queue.empty()) 58 | { 59 | signal.wait_for(lock, std::chrono::milliseconds(_milli)); 60 | if (queue.empty()) 61 | { 62 | return false; 63 | } 64 | } 65 | 66 | _value = queue.front(); 67 | queue.pop(); 68 | return true; 69 | } 70 | 71 | private: 72 | std::queue queue; 73 | mutable std::mutex guard; 74 | std::condition_variable signal; 75 | }; -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | INCLUDE(TestBigEndian) 2 | TEST_BIG_ENDIAN(BIGENDIAN) 3 | IF (BIGENDIAN) 4 | SET(MSGPACK_ENDIAN_BIG_BYTE 1) 5 | SET(MSGPACK_ENDIAN_LITTLE_BYTE 0) 6 | ELSE () 7 | SET(MSGPACK_ENDIAN_BIG_BYTE 0) 8 | SET(MSGPACK_ENDIAN_LITTLE_BYTE 1) 9 | ENDIF () 10 | 11 | CONFIGURE_FILE ( 12 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/cmake/sysdep.h.in 13 | include/msgpack/sysdep.h 14 | @ONLY 15 | ) 16 | 17 | CONFIGURE_FILE ( 18 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/cmake/pack_template.h.in 19 | include/msgpack/pack_template.h 20 | @ONLY 21 | ) 22 | 23 | set(LIBCOSMO_PLUGIN_INCLUDE_DIRS 24 | ${CMAKE_CURRENT_SOURCE_DIR}/include 25 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/reflect-cpp/include 26 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/reflect-cpp/include/rfl/thirdparty 27 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/include 28 | 29 | # for generated sysdep.h 30 | ${CMAKE_CURRENT_BINARY_DIR}/include 31 | ${CMAKE_CURRENT_BINARY_DIR}/include/msgpack 32 | PARENT_SCOPE 33 | ) 34 | set(LIBCOSMO_PLUGIN_SOURCES 35 | ${CMAKE_CURRENT_SOURCE_DIR}/src/cosmo_plugin.cpp 36 | 37 | # https://rfl.getml.com/install/#option-4-include-source-files-into-your-own-build 38 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/reflect-cpp/src/reflectcpp.cpp 39 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/reflect-cpp/src/reflectcpp_json.cpp 40 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/reflect-cpp/src/reflectcpp_msgpack.cpp 41 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/reflect-cpp/src/yyjson.c 42 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/src/objectc.c 43 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/src/unpack.c 44 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/src/version.c 45 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/src/vrefbuffer.c 46 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/msgpack-c/src/zone.c 47 | PARENT_SCOPE 48 | ) -------------------------------------------------------------------------------- /tests/signed-unsigned/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "cosmo_plugin.hpp" 12 | 13 | const int64_t signedValue = 8331091968; 14 | const uint64_t unsignedValue = 8331091968; 15 | 16 | void registerTestHandlers(RPCPeer& peer, bool* done) { 17 | peer.registerHandler("signed", std::function([=](int64_t value) -> int64_t { 18 | return value; 19 | })); 20 | peer.registerHandler("unsigned", std::function([=](uint64_t value) -> uint64_t { 21 | return value; 22 | })); 23 | peer.registerHandler("done", std::function([done]() -> int { 24 | *done = true; 25 | return 1; 26 | })); 27 | } 28 | 29 | void testPeerHandlers(RPCPeer& peer) { 30 | assert(signedValue == peer.call("signed", signedValue)); 31 | assert(unsignedValue == peer.call("unsigned", unsignedValue)); 32 | peer.call("done"); 33 | } 34 | 35 | 36 | #ifdef __COSMOPOLITAN__ 37 | 38 | int main(int argc, char *argv[]) { 39 | if (argc != 2) { 40 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 41 | return 1; 42 | } 43 | 44 | std::string cwd = std::filesystem::current_path(); 45 | std::string objectPath = cwd + "/" + argv[1]; 46 | 47 | std::cout << "Populating host functions..." << std::endl; 48 | PluginHost::ProtocolEncoding encoding = PluginHost::ProtocolEncoding::MSGPACK; 49 | #ifdef USE_JSON_ENCODING 50 | encoding = PluginHost::ProtocolEncoding::JSON; 51 | #endif 52 | PluginHost plugin(objectPath, PluginHost::LaunchMethod::AUTO, encoding); 53 | bool done = false; 54 | registerTestHandlers(plugin, &done); 55 | 56 | std::cout << "Initializing shared library..." << std::endl; 57 | plugin.initialize(); 58 | 59 | std::cout << "Testing plugin functions..." << std::endl; 60 | testPeerHandlers(plugin); 61 | 62 | while(!done) { 63 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 64 | } 65 | 66 | std::cout << "Host exiting..." << std::endl; 67 | return 0; 68 | } 69 | 70 | #else 71 | 72 | void plugin_initializer(Plugin *plugin) { 73 | bool *done = new bool; 74 | *done = false; 75 | registerTestHandlers(*plugin, done); 76 | 77 | std::cout << "Plugin initialized." << std::endl; 78 | 79 | std::thread([plugin, done]() { 80 | std::cout << "Testing host functions..." << std::endl; 81 | testPeerHandlers(*plugin); 82 | 83 | while(!*done) { 84 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 85 | } 86 | 87 | delete done; 88 | std::cout << "Plugin exiting..." << std::endl; 89 | }).detach(); 90 | } 91 | 92 | #endif 93 | 94 | -------------------------------------------------------------------------------- /tests/stress/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "cosmo_plugin.hpp" 10 | 11 | void registerTestHandlers(RPCPeer& peer, bool* done) { 12 | peer.registerHandler("add", std::function([](int a, int b) -> int { 13 | return a + b; 14 | })); 15 | peer.registerHandler("peerAdd", std::function([&peer](int a, int b) -> int { 16 | return peer.call("add", a, b); 17 | })); 18 | peer.registerHandler("done", std::function([done]() -> int { 19 | *done = true; 20 | return 1; 21 | })); 22 | } 23 | 24 | void testPeerHandlers(RPCPeer& peer) { 25 | // Define the task for each thread 26 | auto task = [&peer]() { 27 | for (int i = 0; i < 1000; i++) { 28 | int result = peer.call("add", 2, 3); 29 | assert(result == 5); 30 | } 31 | for (int i = 0; i < 1000; i++) { 32 | int result = peer.call("peerAdd", 2, 3); 33 | assert(result == 5); 34 | } 35 | }; 36 | 37 | // Create 20 threads 38 | std::vector threads; 39 | for (int t = 0; t < 20; t++) { 40 | threads.emplace_back(task); 41 | } 42 | 43 | // Join all threads 44 | for (auto& thread : threads) { 45 | thread.join(); 46 | } 47 | 48 | // Call "done" after all threads are complete 49 | peer.call("done"); 50 | } 51 | 52 | 53 | #ifdef __COSMOPOLITAN__ 54 | 55 | int main(int argc, char *argv[]) { 56 | if (argc != 2) { 57 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 58 | return 1; 59 | } 60 | 61 | std::string cwd = std::filesystem::current_path(); 62 | std::string objectPath = cwd + "/" + argv[1]; 63 | 64 | std::cout << "Populating host functions..." << std::endl; 65 | PluginHost::ProtocolEncoding encoding = PluginHost::ProtocolEncoding::MSGPACK; 66 | #ifdef USE_JSON_ENCODING 67 | encoding = PluginHost::ProtocolEncoding::JSON; 68 | #endif 69 | PluginHost plugin(objectPath, PluginHost::LaunchMethod::AUTO, encoding); 70 | bool done = false; 71 | registerTestHandlers(plugin, &done); 72 | 73 | std::cout << "Initializing shared library..." << std::endl; 74 | plugin.initialize(); 75 | 76 | std::cout << "Testing plugin functions..." << std::endl; 77 | testPeerHandlers(plugin); 78 | 79 | while(!done) { 80 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 81 | } 82 | 83 | std::cout << "Host exiting..." << std::endl; 84 | return 0; 85 | } 86 | 87 | #else 88 | 89 | void plugin_initializer(Plugin *plugin) { 90 | bool *done = new bool; 91 | *done = false; 92 | registerTestHandlers(*plugin, done); 93 | 94 | std::cout << "Plugin initialized." << std::endl; 95 | 96 | std::thread([plugin, done]() { 97 | std::cout << "Testing host functions..." << std::endl; 98 | testPeerHandlers(*plugin); 99 | 100 | while(!*done) { 101 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 102 | } 103 | 104 | delete done; 105 | std::cout << "Plugin exiting..." << std::endl; 106 | }).detach(); 107 | } 108 | 109 | #endif 110 | 111 | -------------------------------------------------------------------------------- /tests/large/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "cosmo_plugin.hpp" 12 | 13 | std::vector generateLargeVector(size_t size) { 14 | std::vector result; 15 | result.reserve(size); 16 | 17 | for (size_t i = 0; i < size; ++i) { 18 | std::ostringstream oss; 19 | oss << "String_" << i; 20 | result.emplace_back(oss.str()); 21 | } 22 | 23 | return result; 24 | } 25 | 26 | bool areVectorsEquivalent(const std::vector& vec1, const std::vector& vec2) { 27 | return vec1.size() == vec2.size() && std::equal(vec1.begin(), vec1.end(), vec2.begin()); 28 | } 29 | 30 | 31 | void registerTestHandlers(RPCPeer& peer, bool* done) { 32 | peer.registerHandler("generate", std::function([](size_t size) -> std::vector { 33 | return generateLargeVector(size); 34 | })); 35 | peer.registerHandler("done", std::function([done]() -> int { 36 | *done = true; 37 | return 1; 38 | })); 39 | } 40 | 41 | void testPeerHandlers(RPCPeer& peer) { 42 | for (int i = 0; i < 1000; i++) { 43 | auto expected = generateLargeVector(i); 44 | auto actual = peer.call>("generate", i); 45 | assert(areVectorsEquivalent(expected, actual)); 46 | } 47 | peer.call("done"); 48 | } 49 | 50 | 51 | #ifdef __COSMOPOLITAN__ 52 | 53 | int main(int argc, char *argv[]) { 54 | if (argc != 2) { 55 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 56 | return 1; 57 | } 58 | 59 | std::string cwd = std::filesystem::current_path(); 60 | std::string objectPath = cwd + "/" + argv[1]; 61 | 62 | std::cout << "Populating host functions..." << std::endl; 63 | PluginHost::ProtocolEncoding encoding = PluginHost::ProtocolEncoding::MSGPACK; 64 | #ifdef USE_JSON_ENCODING 65 | encoding = PluginHost::ProtocolEncoding::JSON; 66 | #endif 67 | PluginHost plugin(objectPath, PluginHost::LaunchMethod::AUTO, encoding); 68 | bool done = false; 69 | registerTestHandlers(plugin, &done); 70 | 71 | std::cout << "Initializing shared library..." << std::endl; 72 | plugin.initialize(); 73 | 74 | std::cout << "Testing plugin functions..." << std::endl; 75 | testPeerHandlers(plugin); 76 | 77 | while(!done) { 78 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 79 | } 80 | 81 | std::cout << "Host exiting..." << std::endl; 82 | return 0; 83 | } 84 | 85 | #else 86 | 87 | void plugin_initializer(Plugin *plugin) { 88 | bool *done = new bool; 89 | *done = false; 90 | registerTestHandlers(*plugin, done); 91 | 92 | std::cout << "Plugin initialized." << std::endl; 93 | 94 | std::thread([plugin, done]() { 95 | std::cout << "Testing host functions..." << std::endl; 96 | testPeerHandlers(*plugin); 97 | 98 | while(!*done) { 99 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 100 | } 101 | 102 | delete done; 103 | std::cout << "Plugin exiting..." << std::endl; 104 | }).detach(); 105 | } 106 | 107 | #endif 108 | 109 | -------------------------------------------------------------------------------- /tests/bidirectional/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "cosmo_plugin.hpp" 10 | 11 | struct MyData { 12 | int a; 13 | std::string b; 14 | }; 15 | 16 | struct ComplexData { 17 | std::vector dataList; 18 | std::map dataMap; 19 | }; 20 | 21 | using namespace std::literals::string_literals; 22 | 23 | void registerTestHandlers(RPCPeer& peer, bool* done) { 24 | peer.registerHandler("add", std::function([](int a, int b) -> int { 25 | return a + b; 26 | })); 27 | peer.registerHandler("subtract", std::function([](int a, int b) -> int { 28 | return a - b; 29 | })); 30 | peer.registerHandler("createData", std::function([](int a, std::string b) -> MyData { 31 | return MyData{a, b}; 32 | })); 33 | peer.registerHandler("processComplexData", std::function([](ComplexData cd) -> std::string { 34 | int total = 0; 35 | for (const auto &item : cd.dataList) { 36 | total += item.a; 37 | } 38 | for (const auto &[key, value] : cd.dataMap) { 39 | total += value; 40 | } 41 | return "Total: " + std::to_string(total); 42 | })); 43 | peer.registerHandler("peerArithmetic", std::function([&peer](int a, int b, int c) -> int { 44 | int result = peer.call("add", a, b); 45 | result = peer.call("subtract", result, c); 46 | return result; 47 | })); 48 | peer.registerHandler("done", std::function([done]() -> int { 49 | *done = true; 50 | return 1; 51 | })); 52 | } 53 | 54 | void testPeerHandlers(RPCPeer& peer) { 55 | int result = peer.call("add", 2, 3); 56 | assert(result == 5); 57 | 58 | result = peer.call("subtract", 10, 5); 59 | assert(result == 5); 60 | 61 | MyData data = peer.call("createData", 10, "20"s); 62 | assert(data.a == 10); 63 | assert(data.b == "20"); 64 | 65 | ComplexData complexData{ 66 | .dataList = {{1, "One"}, {2, "Two"}}, 67 | .dataMap = {{"key1", 10}, {"key2", 20}} 68 | }; 69 | std::string summary = peer.call("processComplexData", complexData); 70 | assert(summary == "Total: 33"); 71 | 72 | result = peer.call("peerArithmetic", 10, 20, 5); 73 | assert(result == 25); 74 | 75 | peer.call("done"); 76 | } 77 | 78 | #ifdef __COSMOPOLITAN__ 79 | 80 | int main(int argc, char *argv[]) { 81 | if (argc != 2) { 82 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 83 | return 1; 84 | } 85 | 86 | std::string cwd = std::filesystem::current_path(); 87 | std::string objectPath = cwd + "/" + argv[1]; 88 | 89 | std::cout << "Populating host functions..." << std::endl; 90 | PluginHost::ProtocolEncoding encoding = PluginHost::ProtocolEncoding::MSGPACK; 91 | #ifdef USE_JSON_ENCODING 92 | encoding = PluginHost::ProtocolEncoding::JSON; 93 | #endif 94 | PluginHost plugin(objectPath, PluginHost::LaunchMethod::AUTO, encoding); 95 | bool done = false; 96 | registerTestHandlers(plugin, &done); 97 | 98 | std::cout << "Initializing shared library..." << std::endl; 99 | plugin.initialize(); 100 | 101 | std::cout << "Testing plugin functions..." << std::endl; 102 | testPeerHandlers(plugin); 103 | 104 | while(!done) { 105 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 106 | } 107 | 108 | std::cout << "Host exiting..." << std::endl; 109 | return 0; 110 | } 111 | 112 | #else 113 | 114 | void plugin_initializer(Plugin *plugin) { 115 | bool *done = new bool; 116 | *done = false; 117 | registerTestHandlers(*plugin, done); 118 | 119 | std::cout << "Plugin initialized." << std::endl; 120 | 121 | std::thread([plugin, done]() { 122 | std::cout << "Testing host functions..." << std::endl; 123 | testPeerHandlers(*plugin); 124 | 125 | while(!*done) { 126 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 127 | } 128 | 129 | delete done; 130 | std::cout << "Plugin exiting..." << std::endl; 131 | }).detach(); 132 | } 133 | 134 | #endif 135 | 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libcosmo_plugin 2 | 3 | ![GitHub License](https://img.shields.io/github/license/bjia56/libcosmo_plugin) 4 | [![Generic badge](https://img.shields.io/badge/C++-20-blue.svg)](https://shields.io/) 5 | 6 | `libcosmo_plugin` is a C++ library for building platform-native plugins usable from Cosmopolitan Libc. 7 | 8 | ## Motivation 9 | 10 | While Cosmopolitan Libc (and the Actually Portable Executable file format) allows creating portable binaries that can run on multiple OSes and architectures, sometimes it is necessary for a program to use platform-specific shared libraries or functions. Cosmopolitan's `cosmo_dlopen` function was introduced to allow calling platform-specific shared libraries, but is limited by only allowing one way communication and cannot expose host process symbols to the library. `libcosmo_plugin` aims to address this limitation by introducing a plugin architecture between host process and native plugin, allowing for bidirectional communication between both parties. 11 | 12 | ## Usage 13 | 14 | Using `libcosmo_plugin` takes different forms depending on whether the library is being built by the Cosmopolitan Libc toolchain (`cosmoc++` and its variants) or by a stock C++ compiler. 15 | 16 | With Cosmopolitan, `libcosmo_plugin` is in "Plugin Host" mode and exposes the `PluginHost` class, which is used to load a plugin shared library (or executable; see [below](#Building)) and export host process functions that will be exposed to the plugin. This class can also be used to call functions exported by the plugin. 17 | 18 | ```c++ 19 | std::string pluginPath = ...; // path to the plugin shared library or executable 20 | PluginHost plugin(pluginPath); 21 | 22 | // configure handlers callable by the plugin 23 | plugin.registerHandler("add", std::function([](int a, int b) -> int { 24 | return a + b; 25 | })); 26 | 27 | // launch the plugin 28 | plugin.initialize(); 29 | 30 | // call functions exposed by the plugin 31 | int result = plugin.call("subtract", 10, 5); 32 | ``` 33 | 34 | With a stock compiler, `libcosmo_plugin` is in "Plugin" mode and exposes the `Plugin` class, which is used to export shared library functions that will be exposed to the host. This class can also be used to call functions exported by the host. Plugin mode also exports certain shared library symbols, which will be used to bootstrap the bidirectional plugin connection. 35 | 36 | Plugins must implement the `void plugin_initializer(Plugin* plugin)` function, which is called by `libcosmo_plugin` on initialization. This is an opportunity to register handlers, etc. before plugin loading is considered complete by the host program. This function *must return* for the host to continue execution, so any long-running tasks should be delegated to additional threads. Notably, calling exposed functions in the host can only be done after the initializer finishes. 37 | 38 | ```c++ 39 | void plugin_initializer(Plugin *plugin) { 40 | // configure handlers callable by the host 41 | plugin->registerHandler("subtract", std::function([](int a, int b) -> int { 42 | return a - b; 43 | })); 44 | 45 | // do long-running tasks and call host functions in a separate thread 46 | std::thread([plugin]() { 47 | // call functions exposed by the host 48 | int result = plugin->call("add", 2, 3); 49 | }).detach(); 50 | } 51 | ``` 52 | 53 | ## Building 54 | 55 | `libcosmo_plugin` relies heavily on C++ templates, so it is recommended to build and link it with your application directly, instead of as a separate library. 56 | 57 | The easiest way to build `libcosmo_plugin` is with CMake. Add this repository as a submodule to your project, then include it as a subdirectory in your `CMakeLists.txt`. The variables `LIBCOSMO_PLUGIN_SOURCES` and `LIBCOSMO_PLUGIN_INCLUDE_DIRS` will be populated with the source files and include directories, respectively, which can be added to your application and shared library (plugin) builds. 58 | 59 | `libcosmo_plugin` relies on `cosmo_dlopen`, which is not available on all platforms supported by Cosmopolitan. Plugins can therefore be built either in "shared library" mode for `cosmo_dlopen` to load the plugin in the process's address space, or in "executable" mode for `posix_spawn` to launch the plugin as a subprocess. Sensible defaults are enabled by `libcosmo_plugin` to leverage `cosmo_dlopen` wherever reliably implemented, and `posix_spawn` elsewhere. The following table lists the defaults: 60 | 61 | | Platform | Launch method | 62 | |-|-| 63 | | Linux | `cosmo_dlopen` | 64 | | MacOS (x86_64) | `posix_spawn` | 65 | | MacOS (arm64) | `cosmo_dlopen` | 66 | | Windows | `cosmo_dlopen` | 67 | | FreeBSD | `cosmo_dlopen` | 68 | | NetBSD | `posix_spawn` | 69 | | OpenBSD | `posix_spawn` | 70 | 71 | Compiling native plugins will follow these defaults when enabling code. Specifically, platforms where `posix_spawn` is default will generate a `main()` function that calls the plugin initialization routines within `libcosmo_plugin`, effectively turning the plugins into executables. To disable this, define `COSMO_PLUGIN_DONT_GENERATE_MAIN` when building `libcosmo_plugin`. On platforms where `cosmo_dlopen` is the default, enabling the `main()` function can be done by defining `COSMO_PLUGIN_WANT_MAIN` when building `libcosmo_plugin`. 72 | 73 | ## License 74 | 75 | `libcosmo_plugin` is released under the MIT License. It also relies on code from the [`reflect-cpp`](https://github.com/getml/reflect-cpp) library; refer to their documentation for up to date licensing information. 76 | -------------------------------------------------------------------------------- /.github/workflows/test_single.yml: -------------------------------------------------------------------------------- 1 | name: Build and run a single test 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | test: 7 | required: true 8 | type: string 9 | encoding: 10 | required: false 11 | type: string 12 | debug_cosmo: 13 | required: false 14 | type: boolean 15 | debug_native: 16 | required: false 17 | type: boolean 18 | workflow_call: 19 | inputs: 20 | test: 21 | required: true 22 | type: string 23 | encoding: 24 | required: false 25 | type: string 26 | 27 | jobs: 28 | build_exe: 29 | name: ${{ inputs.test }}/${{ inputs.encoding }} [cosmo] 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | submodules: recursive 37 | 38 | - name: Load cosmocc version 39 | run: | 40 | version=$(cat .github/cosmocc_version.txt) 41 | echo "cosmocc_version=${version}" >> "$GITHUB_ENV" 42 | 43 | - name: Setup cosmocc 44 | uses: bjia56/setup-cosmocc@v0.0.4 45 | with: 46 | version: ${{ env.cosmocc_version }} 47 | 48 | - name: Build exe ${{ inputs.test }} 49 | run: | 50 | cd tests/${{ inputs.test }} 51 | CC=cosmocc CXX=cosmoc++ cmake -B build -DBUILD_EXE=ON ${{ inputs.encoding == 'json' && '-DUSE_JSON_ENCODING=ON' || '' }} 52 | cmake --build build --parallel 4 53 | 54 | - name: Upload artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: ${{ inputs.test }}-${{ inputs.encoding }}-exe 58 | path: tests/${{ inputs.test }}/build/cosmo.com 59 | 60 | - name: Interactive debugging 61 | uses: fawazahmed0/action-debug-vscode@v3 62 | if: ${{ always() && inputs.debug_cosmo }} 63 | 64 | build_test_native: 65 | name: ${{ inputs.test }}/${{ inputs.encoding }} [${{ matrix.os }} ${{ matrix.arch }}] 66 | needs: build_exe 67 | runs-on: ${{ matrix.runner }} 68 | strategy: 69 | fail-fast: false 70 | matrix: 71 | include: 72 | - runner: macos-13 73 | arch: x86_64 74 | os: MacOS 75 | - runner: macos-14 76 | arch: arm64 77 | os: MacOS 78 | - runner: ubuntu-latest 79 | arch: x86_64 80 | os: Linux 81 | - runner: windows-latest 82 | arch: x86_64 83 | os: Windows 84 | - runner: ubuntu-latest 85 | arch: x86_64 86 | os: FreeBSD 87 | - runner: ubuntu-latest 88 | arch: aarch64 89 | os: FreeBSD 90 | - runner: ubuntu-latest 91 | arch: x86_64 92 | os: NetBSD 93 | 94 | steps: 95 | - name: Set up cosmocc 96 | if: ${{ matrix.os == 'Linux' }} 97 | uses: bjia56/setup-cosmocc@v0.0.4 98 | 99 | - name: Setup cmake 100 | if: ${{ !contains(matrix.os, 'BSD') }} 101 | uses: jwlawson/actions-setup-cmake@v2.0.2 102 | with: 103 | cmake-version: 3.31.3 104 | 105 | - name: Checkout 106 | uses: actions/checkout@v4 107 | with: 108 | submodules: recursive 109 | 110 | - name: Download artifact 111 | uses: actions/download-artifact@v4 112 | with: 113 | name: ${{ inputs.test }}-${{ inputs.encoding }}-exe 114 | path: . 115 | 116 | - name: Mark executable 117 | shell: bash 118 | run: | 119 | chmod +x cosmo.com 120 | 121 | - name: Build native and test 122 | if: ${{ !contains(matrix.os, 'BSD') }} 123 | shell: bash 124 | run: | 125 | cd tests/${{ inputs.test }} 126 | if [[ "${{ matrix.os}}" == "MacOS" && "${{ matrix.arch }}" == "x86_64" ]]; then 127 | cmake -B build -DBUILD_EXE=ON -DBINARY_NAME=native 128 | else 129 | cmake -B build 130 | fi 131 | cmake --build build --parallel 4 132 | 133 | cp ../../cosmo.com . 134 | if [[ "${{ matrix.os }}" == "Windows" ]]; then 135 | time ./cosmo.com build/Debug/native.dll 136 | elif [[ "${{ matrix.os }}" == "MacOS" && "${{ matrix.arch }}" == "x86_64" ]]; then 137 | time ./cosmo.com build/native 138 | else 139 | time ./cosmo.com build/libnative.so 140 | fi 141 | 142 | - name: Start VM 143 | if: ${{ matrix.os == 'FreeBSD' }} 144 | uses: vmactions/freebsd-vm@v1 145 | with: 146 | sync: nfs 147 | arch: ${{ matrix.arch }} 148 | prepare: | 149 | pkg install -y cmake 150 | 151 | - name: Start VM 152 | if: ${{ matrix.os == 'NetBSD' }} 153 | uses: vmactions/netbsd-vm@v1 154 | with: 155 | sync: nfs 156 | prepare: | 157 | /usr/sbin/pkg_add cmake clang 158 | 159 | - name: Build native and test 160 | if: ${{ matrix.os == 'FreeBSD' }} 161 | shell: freebsd {0} 162 | run: | 163 | cd ${{ github.workspace }}/tests/${{ inputs.test }} 164 | cmake -B build 165 | cmake --build build --parallel 4 166 | 167 | cp ../../cosmo.com . 168 | ./cosmo.com build/libnative.so 169 | 170 | - name: Build native and test 171 | if: ${{ matrix.os == 'NetBSD' }} 172 | shell: netbsd {0} 173 | run: | 174 | cd ${{ github.workspace }}/tests/${{ inputs.test }} 175 | CC=clang CXX=clang++ cmake -B build -DBUILD_EXE=ON -DBINARY_NAME=native 176 | cmake --build build --parallel 4 177 | 178 | cp ../../cosmo.com . 179 | ./cosmo.com build/native 180 | 181 | - name: Interactive debugging 182 | uses: fawazahmed0/action-debug-vscode@v3 183 | if: ${{ always() && inputs.debug_native }} 184 | -------------------------------------------------------------------------------- /include/cosmo_plugin.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COSMO_RPC_HPP 2 | #define COSMO_RPC_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #if defined(_MSC_VER) 15 | #include 16 | typedef SSIZE_T ssize_t; 17 | #endif 18 | 19 | #include "LockingQueue.hpp" 20 | 21 | #ifndef __COSMOPOLITAN__ 22 | 23 | #if defined(_MSC_VER) 24 | // Microsoft 25 | #define EXPORT __declspec(dllexport) 26 | #define IMPORT __declspec(dllimport) 27 | #elif defined(__GNUC__) 28 | // GCC 29 | #define EXPORT __attribute__((visibility("default"))) 30 | #define IMPORT 31 | #else 32 | // do nothing and hope for the best? 33 | #define EXPORT 34 | #define IMPORT 35 | #pragma warning Unknown dynamic link import/export semantics. 36 | #endif 37 | 38 | extern "C" EXPORT void cosmo_rpc_initialization(long, long, long); 39 | extern "C" EXPORT void cosmo_rpc_teardown(); 40 | 41 | #endif // __COSMOPOLITAN__ 42 | 43 | class RPCPeer { 44 | public: 45 | enum ProtocolEncoding { 46 | MSGPACK = 0, 47 | JSON 48 | }; 49 | 50 | // Register a handler for a specific method 51 | template 52 | void registerHandler(const std::string& method, std::function handler); 53 | 54 | // Call a method on the peer and get a response 55 | template 56 | ReturnType call(const std::string& method, Args&&... args); 57 | 58 | // Process incoming requests and responses 59 | void processMessages(); 60 | 61 | private: 62 | // Serialize and deserialize RPC messages 63 | struct Message { 64 | enum class Type { 65 | Request, 66 | Response 67 | }; 68 | 69 | unsigned long id; 70 | std::optional method; 71 | std::optional params; 72 | std::optional result; 73 | std::optional error; 74 | }; 75 | 76 | // Keep fields in sync with struct Message 77 | struct JSONMessage { 78 | unsigned long id; 79 | std::optional method; 80 | std::optional params; 81 | std::optional result; 82 | std::optional error; 83 | }; 84 | 85 | // Abstract Transport implementation 86 | struct Transport { 87 | ssize_t (*write)(const void* buffer, size_t size, void* context); 88 | ssize_t (*read)(void* buffer, size_t size, void* context); 89 | void (*close)(void* context); 90 | void* context; // User-provided context (e.g., socket, file descriptor) 91 | }; 92 | 93 | // Transport connecting to the peer 94 | Transport transport; 95 | 96 | // Handlers for incoming requests 97 | std::unordered_map(const std::vector&)>> handlers; 98 | std::mutex handlersMutex; 99 | 100 | // Queue response messages 101 | std::unordered_map*> responseQueue; 102 | std::mutex responseQueueMutex; 103 | 104 | // Request counter 105 | std::atomic requestCounter; 106 | 107 | // Helper messages to send and receive data 108 | void sendMessage(const Message& message); 109 | std::mutex sendMutex; 110 | std::optional receiveMessage(); 111 | void processRequest(const Message& request); 112 | 113 | // Protocol encoding 114 | enum ProtocolEncoding encoding; 115 | 116 | // Helper function to convert string and vector 117 | static std::vector stringToVectorString(const std::string& str) { 118 | return std::vector(str.begin(), str.end()); 119 | } 120 | static std::string vectorStringToString(const std::vector& vec) { 121 | return std::string(vec.begin(), vec.end()); 122 | } 123 | 124 | #ifdef __COSMOPOLITAN__ 125 | friend class PluginHost; 126 | #else 127 | friend class Plugin; 128 | friend void cosmo_rpc_initialization(long, long, long); 129 | #endif // __COSMOPOLITAN__ 130 | friend class MockPeer; 131 | }; 132 | 133 | #ifdef __COSMOPOLITAN__ 134 | 135 | class PluginHost : public RPCPeer { 136 | public: 137 | enum LaunchMethod { 138 | AUTO = 0, 139 | DLOPEN, 140 | FORK 141 | }; 142 | 143 | PluginHost(const std::string& pluginPath, LaunchMethod launchMethod = AUTO, ProtocolEncoding encoding = MSGPACK); 144 | ~PluginHost(); 145 | 146 | void initialize(); 147 | 148 | private: 149 | std::string pluginPath; 150 | enum LaunchMethod launchMethod; 151 | 152 | struct impl; 153 | std::unique_ptr pimpl; 154 | }; 155 | 156 | #else 157 | 158 | class Plugin : public RPCPeer { 159 | public: 160 | ~Plugin(); 161 | 162 | private: 163 | Plugin(ProtocolEncoding encoding); 164 | 165 | friend void cosmo_rpc_initialization(long, long, long); 166 | }; 167 | 168 | #endif // __COSMOPOLITAN__ 169 | 170 | class MockPeer : public RPCPeer { 171 | public: 172 | MockPeer(); 173 | ~MockPeer(); 174 | 175 | private: 176 | struct impl; 177 | std::unique_ptr pimpl; 178 | }; 179 | 180 | template 181 | void RPCPeer::registerHandler(const std::string& method, std::function handler) { 182 | std::lock_guard lock(handlersMutex); 183 | handlers[method] = [handler, this](const std::vector& params) -> std::vector { 184 | // Deserialize the arguments from the encoding format 185 | std::tuple args; 186 | if (encoding == MSGPACK) { 187 | args = rfl::msgpack::read, rfl::NoFieldNames>(params).value(); 188 | } else { 189 | std::string paramsStr = vectorStringToString(params); 190 | args = rfl::json::read>(paramsStr).value(); 191 | } 192 | 193 | // Call the handler with the deserialized arguments 194 | ReturnType result = std::apply(handler, args); 195 | 196 | // Serialize the result into the encoding format 197 | return ( 198 | encoding == MSGPACK ? rfl::msgpack::write(result) 199 | : stringToVectorString(rfl::json::write(result)) 200 | ); 201 | }; 202 | } 203 | 204 | template 205 | ReturnType RPCPeer::call(const std::string& method, Args&&... args) { 206 | // Generate a unique request ID 207 | const unsigned long requestID = ++requestCounter; 208 | 209 | // Build the RPC request 210 | Message msg{ 211 | .id = requestID, 212 | .method = method, 213 | .params = ( 214 | encoding == MSGPACK ? rfl::msgpack::write(std::make_tuple(std::forward(args)...)) 215 | : stringToVectorString(rfl::json::write(std::make_tuple(std::forward(args)...))) 216 | ) 217 | }; 218 | 219 | // Prepare response handler 220 | LockingQueue queue; 221 | { 222 | std::lock_guard lock(responseQueueMutex); 223 | responseQueue[requestID] = &queue; 224 | } 225 | 226 | sendMessage(msg); 227 | 228 | // Wait for the response 229 | Message msgResponse; 230 | queue.waitAndPop(msgResponse); 231 | 232 | // Remove the response handler 233 | { 234 | std::lock_guard lock(responseQueueMutex); 235 | responseQueue.erase(requestID); 236 | } 237 | 238 | // Check for errors in the response 239 | if (msgResponse.error.has_value()) { 240 | throw std::runtime_error("RPC error: " + msgResponse.error.value()); 241 | } 242 | if (!msgResponse.result.has_value()) { 243 | throw std::runtime_error("RPC response missing result"); 244 | } 245 | 246 | // Deserialize the result into the expected return type 247 | ReturnType result; 248 | if (encoding == MSGPACK) { 249 | result = rfl::msgpack::read(msgResponse.result.value()).value(); 250 | } else { 251 | std::string resultStr = vectorStringToString(msgResponse.result.value()); 252 | result = rfl::json::read(resultStr).value(); 253 | } 254 | return result; 255 | } 256 | 257 | #ifndef __COSMOPOLITAN__ 258 | 259 | // Must be defined in the shared object 260 | void plugin_initializer(Plugin* plugin); 261 | 262 | #endif // __COSMOPOLITAN__ 263 | 264 | #endif // COSMO_RPC_HPP 265 | -------------------------------------------------------------------------------- /src/cosmo_plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "cosmo_plugin.hpp" 2 | 3 | #ifdef __COSMOPOLITAN__ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | class PipeManager { 24 | public: 25 | PipeManager() { 26 | if (IsWindows()) { 27 | // nt 28 | // ensure the pipes are created with inheritable handles 29 | struct NtSecurityAttributes sa = {sizeof(struct NtSecurityAttributes), nullptr, true}; 30 | auto result = CreatePipe(&hostPipeFDs[0], &hostPipeFDs[1], &sa, 0); 31 | if (!result) { 32 | throw std::runtime_error("Failed to create host pipe: " + std::to_string(GetLastError())); 33 | } 34 | result = CreatePipe(&pluginPipeFDs[0], &pluginPipeFDs[1], &sa, 0); 35 | if (!result) { 36 | throw std::runtime_error("Failed to create plugin pipe: " + std::to_string(GetLastError())); 37 | } 38 | } else { 39 | // unix 40 | int fds[2]; 41 | if (pipe(fds) == -1) { 42 | throw std::runtime_error("Failed to create host pipe: " + std::string(strerror(errno))); 43 | } 44 | hostPipeFDs[0] = fds[0]; 45 | hostPipeFDs[1] = fds[1]; 46 | if (pipe(fds) == -1) { 47 | throw std::runtime_error("Failed to create plugin pipe: " + std::string(strerror(errno))); 48 | } 49 | pluginPipeFDs[0] = fds[0]; 50 | pluginPipeFDs[1] = fds[1]; 51 | } 52 | } 53 | 54 | ~PipeManager() { 55 | closePipes(); 56 | } 57 | 58 | void closePipes() { 59 | closing = true; 60 | 61 | if (IsWindows()) { 62 | // nt 63 | if (hostPipeFDs[0] != -1) { 64 | CloseHandle(hostPipeFDs[0]); 65 | hostPipeFDs[0] = -1; 66 | } 67 | if (hostPipeFDs[1] != -1) { 68 | CloseHandle(hostPipeFDs[1]); 69 | hostPipeFDs[1] = -1; 70 | } 71 | if (pluginPipeFDs[0] != -1) { 72 | CloseHandle(pluginPipeFDs[0]); 73 | pluginPipeFDs[0] = -1; 74 | } 75 | if (pluginPipeFDs[1] != -1) { 76 | CloseHandle(pluginPipeFDs[1]); 77 | pluginPipeFDs[1] = -1; 78 | } 79 | } else { 80 | // unix 81 | if (hostPipeFDs[0] != -1) { 82 | close((int)hostPipeFDs[0]); 83 | hostPipeFDs[0] = -1; 84 | } 85 | if (hostPipeFDs[1] != -1) { 86 | close((int)hostPipeFDs[1]); 87 | hostPipeFDs[1] = -1; 88 | } 89 | if (pluginPipeFDs[0] != -1) { 90 | close((int)pluginPipeFDs[0]); 91 | pluginPipeFDs[0] = -1; 92 | } 93 | if (pluginPipeFDs[1] != -1) { 94 | close((int)pluginPipeFDs[1]); 95 | pluginPipeFDs[1] = -1; 96 | } 97 | } 98 | } 99 | 100 | long getHostReadFD() const { 101 | return hostPipeFDs[0]; 102 | } 103 | 104 | long getHostWriteFD() const { 105 | return pluginPipeFDs[1]; 106 | } 107 | 108 | long getPluginReadFD() const { 109 | return pluginPipeFDs[0]; 110 | } 111 | 112 | long getPluginWriteFD() const { 113 | return hostPipeFDs[1]; 114 | } 115 | 116 | bool isClosing() const { 117 | return closing; 118 | } 119 | 120 | private: 121 | // use longs to match Windows handles 122 | long hostPipeFDs[2] = {-1, -1}; 123 | long pluginPipeFDs[2] = {-1, -1}; 124 | 125 | // indicate to Windows when we want to interrupt reads 126 | bool closing = false; 127 | }; 128 | 129 | static pid_t launchSubprocessWithEnv(const char* program, const char* argv[], const char* newEnvVar) { 130 | if (!newEnvVar) { 131 | pid_t pid; 132 | int status = posix_spawn(&pid, program, nullptr, nullptr, const_cast(argv), nullptr); 133 | if (status != 0) { 134 | throw std::runtime_error("Failed to spawn process: " + std::string(strerror(status))); 135 | } 136 | return pid; 137 | } 138 | 139 | // Step 1: Count existing environment variables 140 | size_t envCount = 0; 141 | while (environ[envCount] != nullptr) { 142 | envCount++; 143 | } 144 | 145 | // Step 2: Allocate memory for the new environment 146 | std::vector newEnv(envCount + 2); // +1 for the new variable, +1 for null terminator 147 | 148 | // Step 3: Copy existing environment variables 149 | for (size_t i = 0; i < envCount; i++) { 150 | newEnv[i] = environ[i]; 151 | } 152 | 153 | // Step 4: Add the new environment variable 154 | newEnv[envCount] = newEnvVar; 155 | newEnv[envCount + 1] = nullptr; // Null terminator 156 | 157 | // Step 5: Spawn the subprocess with the new environment 158 | pid_t pid; 159 | int status = posix_spawn(&pid, program, nullptr, nullptr, const_cast(argv), const_cast(newEnv.data())); 160 | 161 | if (status != 0) { 162 | throw std::runtime_error("Failed to spawn process: " + std::string(strerror(status))); 163 | } 164 | 165 | return pid; 166 | } 167 | 168 | struct PluginHost::impl { 169 | void* dynlibHandle = nullptr; 170 | void (*cosmo_rpc_initialization)(long, long, long); 171 | void (*cosmo_rpc_teardown)(); 172 | 173 | int childPID = 0; 174 | 175 | std::shared_ptr pipeMgr = nullptr; 176 | 177 | ~impl() { 178 | if (pipeMgr) { 179 | pipeMgr->closePipes(); 180 | } 181 | 182 | if (dynlibHandle) { 183 | cosmo_rpc_teardown(); 184 | cosmo_dlclose(dynlibHandle); 185 | } 186 | 187 | if (childPID) { 188 | kill(childPID, SIGKILL); 189 | waitpid(childPID, nullptr, 0); 190 | } 191 | } 192 | }; 193 | 194 | PluginHost::PluginHost(const std::string& pluginPath, PluginHost::LaunchMethod launchMethod, PluginHost::ProtocolEncoding encoding) : pluginPath(pluginPath), pimpl(new impl) { 195 | if (launchMethod == AUTO) { 196 | if (IsXnu() && !IsXnuSilicon()) { 197 | launchMethod = FORK; 198 | } else if (IsOpenbsd() || IsNetbsd()) { // netbsd dlopen seems broken 199 | launchMethod = FORK; 200 | } else { 201 | launchMethod = DLOPEN; 202 | } 203 | } 204 | this->launchMethod = launchMethod; 205 | this->encoding = encoding; 206 | } 207 | 208 | PluginHost::~PluginHost() {} 209 | 210 | void PluginHost::initialize() { 211 | // Create our pipe manager 212 | // Use a pointer to a shared_ptr to ensure it is not deleted until the thread is done 213 | std::shared_ptr *pipeMgr = new std::shared_ptr(new PipeManager()); 214 | pimpl->pipeMgr = *pipeMgr; 215 | 216 | if (launchMethod == DLOPEN) { 217 | // Load the shared object 218 | pimpl->dynlibHandle = cosmo_dlopen(pluginPath.c_str(), RTLD_LOCAL | RTLD_NOW); 219 | if (!pimpl->dynlibHandle) { 220 | throw std::runtime_error("Failed to load shared object: " + std::string(cosmo_dlerror())); 221 | } 222 | 223 | // Get the address of the cosmo_rpc_initialization function 224 | pimpl->cosmo_rpc_initialization = reinterpret_cast(cosmo_dltramp(cosmo_dlsym(pimpl->dynlibHandle, "cosmo_rpc_initialization"))); 225 | if (!pimpl->cosmo_rpc_initialization) { 226 | throw std::runtime_error("Failed to find symbol: cosmo_rpc_initialization: " + std::string(cosmo_dlerror())); 227 | } 228 | 229 | // Get the address of the cosmo_rpc_teardown function 230 | pimpl->cosmo_rpc_teardown = reinterpret_cast(cosmo_dltramp(cosmo_dlsym(pimpl->dynlibHandle, "cosmo_rpc_teardown"))); 231 | if (!pimpl->cosmo_rpc_teardown) { 232 | throw std::runtime_error("Failed to find symbol: cosmo_rpc_teardown: " + std::string(cosmo_dlerror())); 233 | } 234 | 235 | // Call the cosmo_rpc_initialization function 236 | pimpl->cosmo_rpc_initialization(pimpl->pipeMgr->getPluginReadFD(), pimpl->pipeMgr->getPluginWriteFD(), static_cast(encoding)); 237 | } else if (launchMethod == FORK) { 238 | // posix_spawn a child process 239 | int pid; 240 | std::string readFD = std::to_string(pimpl->pipeMgr->getPluginReadFD()); 241 | std::string writeFD = std::to_string(pimpl->pipeMgr->getPluginWriteFD()); 242 | std::string encodingStr = std::to_string(static_cast(encoding)); 243 | 244 | const char* argv[] = {pluginPath.c_str(), readFD.c_str(), writeFD.c_str(), encodingStr.c_str(), nullptr}; 245 | 246 | pid = launchSubprocessWithEnv(pluginPath.c_str(), argv, nullptr); 247 | pimpl->childPID = pid; 248 | } else { 249 | throw std::runtime_error("Unsupported launch method."); 250 | } 251 | 252 | // Create the transport 253 | if (IsWindows()) { 254 | transport.write = [](const void* buffer, size_t size, void* context) -> ssize_t { 255 | auto mgr = *static_cast*>(context); 256 | uint32_t bytesWritten; 257 | return WriteFile(mgr->getHostWriteFD(), buffer, size, &bytesWritten, nullptr) ? bytesWritten : -1; 258 | }; 259 | transport.read = [](void* buffer, size_t size, void* context) -> ssize_t { 260 | auto mgr = *static_cast*>(context); 261 | 262 | // loop peek until we get data 263 | while (true) { 264 | if (mgr->isClosing()) { 265 | return -1; 266 | } 267 | uint32_t bytesAvailable; 268 | if (!PeekNamedPipe(mgr->getHostReadFD(), nullptr, 0, nullptr, &bytesAvailable, nullptr)) { 269 | return -1; 270 | } 271 | if (bytesAvailable > 0) { 272 | break; 273 | } 274 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 275 | } 276 | 277 | uint32_t bytesRead; 278 | return ReadFile(mgr->getHostReadFD(), buffer, size, &bytesRead, nullptr) ? bytesRead : -1; 279 | }; 280 | } else { 281 | transport.write = [](const void* buffer, size_t size, void* context) -> ssize_t { 282 | auto mgr = *static_cast*>(context); 283 | return write((int)mgr->getHostWriteFD(), buffer, size); 284 | }; 285 | transport.read = [](void* buffer, size_t size, void* context) -> ssize_t { 286 | auto mgr = *static_cast*>(context); 287 | return read((int)mgr->getHostReadFD(), buffer, size); 288 | }; 289 | } 290 | transport.close = [](void* context) { 291 | auto mgr = *static_cast*>(context); 292 | mgr->closePipes(); 293 | }; 294 | transport.context = pipeMgr; 295 | 296 | // Start thread 297 | std::thread([this, pipeMgr]() { 298 | try { 299 | processMessages(); 300 | //std::cout << "Host thread ended." << std::endl; 301 | } catch (const std::exception& ex) { 302 | std::cerr << "Error processing messages: " << ex.what() << std::endl; 303 | } 304 | // Clean up the pipe manager 305 | if (pipeMgr) { 306 | delete pipeMgr; 307 | } 308 | }).detach(); 309 | } 310 | 311 | #else // __COSMOPOLITAN__ 312 | 313 | #if !defined(COSMO_PLUGIN_DONT_GENERATE_MAIN) && !defined(COSMO_PLUGIN_WANT_MAIN) 314 | # if defined(__APPLE__) && defined(__x86_64__) 315 | # define COSMO_PLUGIN_WANT_MAIN 316 | # elif defined(__OpenBSD__) || defined(__NetBSD__) 317 | # define COSMO_PLUGIN_WANT_MAIN 318 | # endif 319 | #endif // COSMO_PLUGIN_DONT_GENERATE_MAIN 320 | 321 | #ifdef _WIN32 322 | #include 323 | #include 324 | #include 325 | #else 326 | #include 327 | #include 328 | #include 329 | #include 330 | #endif 331 | 332 | #include 333 | #include 334 | #include 335 | #include 336 | #include 337 | #include 338 | 339 | struct IOManager { 340 | #ifdef _WIN32 341 | std::pair fds; 342 | #else 343 | std::pair fds; 344 | #endif 345 | bool isClosing = false; 346 | std::thread* processingThread; 347 | }; 348 | 349 | Plugin::Plugin(Plugin::ProtocolEncoding encoding) { 350 | this->encoding = encoding; 351 | } 352 | 353 | Plugin::~Plugin() { 354 | if (transport.context) { 355 | IOManager* mgr = static_cast(transport.context); 356 | mgr->isClosing = true; 357 | 358 | transport.close(transport.context); 359 | 360 | mgr->processingThread->join(); 361 | delete mgr->processingThread; 362 | delete mgr; 363 | 364 | transport.context = nullptr; 365 | } 366 | } 367 | 368 | struct SharedObjectContext { 369 | Plugin *plugin; 370 | }; 371 | 372 | SharedObjectContext *sharedObjectContext = nullptr; 373 | 374 | extern "C" EXPORT void cosmo_rpc_initialization(long readFD, long writeFD, long encoding) { 375 | Plugin* plugin = new Plugin(Plugin::ProtocolEncoding(encoding)); 376 | 377 | RPCPeer::Transport transport; 378 | #ifdef _WIN32 379 | transport.write = [](const void* buffer, size_t size, void* context) -> ssize_t { 380 | HANDLE writeFD = static_cast(context)->fds.second; 381 | DWORD bytesWritten; 382 | if (!WriteFile(writeFD, buffer, size, &bytesWritten, nullptr)) { 383 | return -1; 384 | } 385 | return bytesWritten; 386 | }; 387 | transport.read = [](void* buffer, size_t size, void* context) -> ssize_t { 388 | IOManager* ioManager = static_cast(context); 389 | HANDLE readFD = ioManager->fds.first; 390 | 391 | // loop peek until we get data 392 | while (!ioManager->isClosing) { 393 | DWORD bytesAvailable; 394 | if (!PeekNamedPipe(readFD, nullptr, 0, nullptr, &bytesAvailable, nullptr)) { 395 | return -1; 396 | } 397 | if (bytesAvailable > 0) { 398 | DWORD bytesRead; 399 | if (!ReadFile(readFD, buffer, size, &bytesRead, nullptr)) { 400 | return -1; 401 | } 402 | return bytesRead; 403 | } 404 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 405 | } 406 | 407 | return -1; 408 | }; 409 | transport.close = [](void* context) { 410 | IOManager* mgr = static_cast(context); 411 | CloseHandle(mgr->fds.first); 412 | CloseHandle(mgr->fds.second); 413 | }; 414 | transport.context = new IOManager{{(HANDLE)readFD, (HANDLE)writeFD}}; 415 | #else 416 | transport.write = [](const void* buffer, size_t size, void* context) -> ssize_t { 417 | int writeFD = static_cast(context)->fds.second; 418 | return write(writeFD, buffer, size); 419 | }; 420 | transport.read = [](void* buffer, size_t size, void* context) -> ssize_t { 421 | IOManager* ioManager = static_cast(context); 422 | int readFD = ioManager->fds.first; 423 | 424 | while (!ioManager->isClosing) { 425 | fd_set readfds; 426 | FD_ZERO(&readfds); 427 | FD_SET(readFD, &readfds); 428 | 429 | // Set up the timeout for 100ms 430 | struct timeval timeout; 431 | timeout.tv_sec = 0; 432 | timeout.tv_usec = 100000; // 100ms in microseconds 433 | 434 | // Use select to wait for the file descriptor to become ready 435 | int result = select(readFD + 1, &readfds, nullptr, nullptr, &timeout); 436 | if (result > 0) { 437 | if (FD_ISSET(readFD, &readfds)) { 438 | // File descriptor is ready for reading 439 | return read(readFD, buffer, size); 440 | } 441 | } else if (result == 0) { 442 | // Timeout occurred 443 | continue; 444 | } 445 | } 446 | 447 | // An error occurred in select 448 | return -1; 449 | }; 450 | transport.close = [](void* context) { 451 | IOManager* mgr = static_cast(context); 452 | close(mgr->fds.first); 453 | close(mgr->fds.second); 454 | }; 455 | transport.context = new IOManager{{(int)readFD, (int)writeFD}}; 456 | #endif 457 | plugin->transport = transport; 458 | 459 | // Pass the Plugin to the shared library initialization function 460 | try { 461 | plugin_initializer(plugin); 462 | } catch (const std::exception& ex) { 463 | std::cerr << "Error during shared library initialization: " << ex.what() << std::endl; 464 | delete plugin; 465 | exit(EXIT_FAILURE); 466 | } 467 | 468 | // Process incoming messages in a thread 469 | ((IOManager*)(transport.context))->processingThread = new std::thread([plugin]() { 470 | try { 471 | plugin->processMessages(); 472 | //std::cout << "Client thread ended." << std::endl; 473 | } catch (const std::exception& ex) { 474 | std::cerr << "Error processing messages: " << ex.what() << std::endl; 475 | } 476 | }); 477 | 478 | // Store the shared object context 479 | sharedObjectContext = new SharedObjectContext{plugin}; 480 | } 481 | 482 | extern "C" EXPORT void cosmo_rpc_teardown() { 483 | if (sharedObjectContext) { 484 | if (sharedObjectContext->plugin) { 485 | delete sharedObjectContext->plugin; 486 | } 487 | delete sharedObjectContext; 488 | sharedObjectContext = nullptr; 489 | } 490 | } 491 | 492 | #ifdef COSMO_PLUGIN_WANT_MAIN 493 | 494 | int main(int argc, char* argv[]) { 495 | if (argc != 4) { 496 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 497 | return 1; 498 | } 499 | 500 | long readFD = atol(argv[1]); 501 | long writeFD = atol(argv[2]); 502 | long encoding = atol(argv[3]); 503 | 504 | if (readFD <= 0 || writeFD <= 0) { 505 | std::cerr << "Invalid file descriptor." << std::endl; 506 | return 1; 507 | } 508 | 509 | cosmo_rpc_initialization(readFD, writeFD, encoding); 510 | 511 | while(sharedObjectContext) { 512 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 513 | } 514 | 515 | return 0; 516 | } 517 | 518 | #endif // COSMO_PLUGIN_WANT_MAIN 519 | 520 | #endif // __COSMOPOLITAN__ 521 | 522 | #if defined(_MSC_VER) 523 | #include 524 | #endif 525 | 526 | void RPCPeer::sendMessage(const Message& message) { 527 | std::lock_guard lock(sendMutex); 528 | const std::vector messageStr = [message, this]() { 529 | if (encoding == MSGPACK) { 530 | return rfl::msgpack::write(message); 531 | } else { 532 | JSONMessage msg = { 533 | .id = message.id, 534 | .method = message.method, 535 | .params = message.params.has_value() ? std::make_optional(vectorStringToString(message.params.value())) : std::nullopt, 536 | .result = message.result.has_value() ? std::make_optional(vectorStringToString(message.result.value())) : std::nullopt, 537 | .error = message.error 538 | }; 539 | return stringToVectorString(rfl::json::write(msg)); 540 | } 541 | }(); 542 | 543 | uint32_t messageSize = htonl(messageStr.size()); 544 | ssize_t bytesSent = transport.write(&messageSize, sizeof(messageSize), transport.context); 545 | if (bytesSent == -1 || static_cast(bytesSent) != sizeof(messageSize)) { 546 | throw std::runtime_error("Failed to send message size."); 547 | } 548 | 549 | bytesSent = transport.write(messageStr.data(), messageStr.size(), transport.context); 550 | if (bytesSent == -1 || static_cast(bytesSent) != messageStr.size()) { 551 | throw std::runtime_error("Failed to send message."); 552 | } 553 | } 554 | 555 | std::optional RPCPeer::receiveMessage() { 556 | // Read the message size first 557 | uint32_t messageSize; 558 | size_t totalBytesReceived = 0; 559 | 560 | // Read the message size (keep reading until we get the complete size) 561 | while (totalBytesReceived < sizeof(messageSize)) { 562 | ssize_t bytesReceived = transport.read( 563 | reinterpret_cast(&messageSize) + totalBytesReceived, 564 | sizeof(messageSize) - totalBytesReceived, 565 | transport.context 566 | ); 567 | 568 | if (bytesReceived <= 0) { 569 | // Connection closed or error reading message size 570 | return {}; 571 | } 572 | 573 | totalBytesReceived += bytesReceived; 574 | } 575 | 576 | messageSize = ntohl(messageSize); 577 | 578 | // Allocate a buffer of the exact message size 579 | std::vector messageBuffer(messageSize); 580 | totalBytesReceived = 0; 581 | 582 | // Read the message data (keep reading until we get the complete message) 583 | while (totalBytesReceived < messageSize) { 584 | ssize_t bytesReceived = transport.read( 585 | messageBuffer.data() + totalBytesReceived, 586 | messageSize - totalBytesReceived, 587 | transport.context 588 | ); 589 | 590 | if (bytesReceived <= 0) { 591 | // Error reading message data 592 | return {}; 593 | } 594 | 595 | totalBytesReceived += bytesReceived; 596 | } 597 | 598 | // Parse the message 599 | auto parsed = [messageBuffer, this]() -> rfl::Result { 600 | if (encoding == MSGPACK) { 601 | return rfl::msgpack::read(messageBuffer); 602 | } else { 603 | std::string messageStr = vectorStringToString(messageBuffer); 604 | auto res = rfl::json::read(messageStr); 605 | if (!res.has_value()) { 606 | return rfl::Result(rfl::Unexpected(res.error())); 607 | } 608 | JSONMessage jsonMsg = res.value(); 609 | return Message{ 610 | .id = jsonMsg.id, 611 | .method = jsonMsg.method, 612 | .params = jsonMsg.params.has_value() ? std::make_optional(stringToVectorString(jsonMsg.params.value())) : std::nullopt, 613 | .result = jsonMsg.result.has_value() ? std::make_optional(stringToVectorString(jsonMsg.result.value())) : std::nullopt, 614 | .error = jsonMsg.error 615 | }; 616 | } 617 | }(); 618 | if (!parsed.has_value()) { 619 | throw std::runtime_error("Failed to parse RPC message"); 620 | } 621 | 622 | 623 | return parsed.value(); 624 | } 625 | 626 | void RPCPeer::processMessages() { 627 | while (true) { 628 | const std::optional maybeMessage = receiveMessage(); 629 | if (!maybeMessage.has_value()) { 630 | break; 631 | } 632 | 633 | const Message msg = maybeMessage.value(); 634 | 635 | if (msg.method.has_value()) { 636 | std::thread([this, msg]() { 637 | processRequest(msg); 638 | }).detach(); 639 | } else if (msg.id) { 640 | std::lock_guard lock(responseQueueMutex); 641 | if (auto it = responseQueue.find(msg.id); it != responseQueue.end()) { 642 | it->second->push(msg); 643 | } 644 | } else { 645 | throw std::runtime_error("Invalid RPC message format."); 646 | } 647 | } 648 | } 649 | 650 | void RPCPeer::processRequest(const Message& request) { 651 | Message msg; 652 | const std::string &method = request.method.value(); 653 | try { 654 | std::function(const std::vector&)> handler; 655 | { 656 | std::lock_guard lock(handlersMutex); 657 | if (auto it = handlers.find(method); it == handlers.end()) { 658 | throw std::runtime_error("Method not found: " + method); 659 | } else { 660 | handler = it->second; 661 | } 662 | } 663 | 664 | msg = Message{ 665 | .id = request.id, 666 | .result = handler(request.params.value()), 667 | .error = std::nullopt 668 | }; 669 | } catch (const std::exception& ex) { 670 | std::cerr << "Error processing request: " << ex.what() << std::endl; 671 | msg = Message{ 672 | .id = request.id, 673 | .result = std::nullopt, 674 | .error = ex.what() 675 | }; 676 | } 677 | sendMessage(msg); 678 | } 679 | 680 | #if defined(__COSMOPOLITAN__) || !defined(_WIN32) 681 | 682 | #include 683 | #include 684 | 685 | struct MockPeer::impl { 686 | int fds[2] = {-1, -1}; 687 | 688 | ~impl() { 689 | if (fds[0] != -1) { 690 | close(fds[0]); 691 | } 692 | if (fds[1] != -1) { 693 | close(fds[1]); 694 | } 695 | }; 696 | }; 697 | 698 | MockPeer::MockPeer() : pimpl(new impl) { 699 | // use socketpair to create a pair of connected sockets 700 | if (socketpair(AF_UNIX, SOCK_STREAM, 0, pimpl->fds) == -1) { 701 | throw std::runtime_error("Failed to create socket pair: " + std::string(strerror(errno))); 702 | } 703 | 704 | transport.write = [](const void* buffer, size_t size, void* context) -> ssize_t { 705 | int writeFD = static_cast(context)->fds[1]; 706 | return write(writeFD, buffer, size); 707 | }; 708 | transport.read = [](void* buffer, size_t size, void* context) -> ssize_t { 709 | int readFD = static_cast(context)->fds[0]; 710 | return read(readFD, buffer, size); 711 | }; 712 | transport.close = [](void* context) { 713 | impl* mgr = static_cast(context); 714 | close(mgr->fds[0]); 715 | close(mgr->fds[1]); 716 | }; 717 | transport.context = pimpl.get(); 718 | 719 | // Start thread 720 | std::thread([this]() { 721 | try { 722 | processMessages(); 723 | //std::cout << "MockPeer thread ended." << std::endl; 724 | } catch (const std::exception& ex) { 725 | std::cerr << "Error processing messages: " << ex.what() << std::endl; 726 | } 727 | }).detach(); 728 | } 729 | 730 | MockPeer::~MockPeer() {} 731 | 732 | #endif // __COSMOPOLITAN__ || !_WIN32 --------------------------------------------------------------------------------