From e0b12c97291f5a9b56c0f618c1a646f2f82d10e3 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Sat, 21 Dec 2024 21:40:20 -0500 Subject: [PATCH] split the "C++ client bindings" properly There probably isn't that much utility to doing this, but now the shared/ code can be imported to write a hazelnut client application in C++. Not that that's of any use to me, but seperating it means it should also be easier to work on. No more strange compile errors because clangd is confused and dumb. --- client/build.rs | 5 ++ client/src/client.rs | 6 +-- client/src/rust_wrapper.cpp | 96 ++++++--------------------------- shared/README.md | 4 +- shared/src/client.cpp | 78 +++++++++++++++++++++++++++ shared/src/client.hpp | 40 ++++++++++++++ shared/src/ivshmem_protocol.hpp | 4 ++ 7 files changed, 149 insertions(+), 84 deletions(-) create mode 100644 shared/src/client.cpp create mode 100644 shared/src/client.hpp diff --git a/client/build.rs b/client/build.rs index 90bb452..68e8d33 100644 --- a/client/build.rs +++ b/client/build.rs @@ -7,12 +7,17 @@ fn main() { println!("cargo:rerun-if-changed=src/rust_wrapper.cpp"); println!("cargo:rerun-if-changed=../shared/src"); + // Build the required shared C++ bits, and the glue C++ code + // that we can import to Rust. (Since the API surface is so small, + // and not in danger of changing in a huge way, I am opting for manual + // bindings instead of using a crate like the `cxx` crate. Sue me.) build .emit_rerun_if_env_changed(true) .cpp(true) .std("c++20") .include("../shared/src") .file("../shared/src/ivshmem.cpp") + .file("../shared/src/client.cpp") .file("src/rust_wrapper.cpp") .compile("hazelnut_client_cpp_native"); } diff --git a/client/src/client.rs b/client/src/client.rs index b7dd89b..328d329 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -6,7 +6,7 @@ pub(crate) mod sys { #[repr(u32)] #[allow(unused)] // it IS used, just not by Rust - pub enum ResultCode { + pub enum ClientResultCode { /// The frame has not been changed. (This is implemented internally by comparing the frame serial; if it has changed then the frame has too) Unchanged, @@ -26,7 +26,7 @@ pub(crate) mod sys { pub(crate) fn rust_hazelnut_client_open(client: *mut ffi::c_void, fd: ffi::c_int) -> bool; - pub(crate) fn rust_hazelnut_client_tick(client: *mut ffi::c_void) -> ResultCode; + pub(crate) fn rust_hazelnut_client_tick(client: *mut ffi::c_void) -> ClientResultCode; // Explicitly lock/unlock. pub(crate) fn rust_hazelnut_client_lock(client: *mut ffi::c_void); @@ -39,7 +39,7 @@ pub(crate) mod sys { } } -pub type ResultCode = sys::ResultCode; +pub type ResultCode = sys::ClientResultCode; /// A Hazelnut IVSHMEM client. Wraps the C++ code (see ./rust_wrapper.cpp and the shared sources) /// so that we can use it in Rust, semi-safely. (Note that I really do not like the current diff --git a/client/src/rust_wrapper.cpp b/client/src/rust_wrapper.cpp index 7109825..c7f3ea8 100644 --- a/client/src/rust_wrapper.cpp +++ b/client/src/rust_wrapper.cpp @@ -1,84 +1,20 @@ -#include "ivshmem.hpp" -#include "ivshmem_protocol.hpp" -#include "Utils.hpp" +#include "client.hpp" -enum class ResultCode : u32 { Unchanged, Changed, LockContended, Fail }; - -struct HazelnutIvshmemClient { - bool Open(int fd) { - if(!ivshmemDevice.Open(fd)) - return false; - - auto* ptr = (u8*)ivshmemDevice.GetPointer(); - - pHeader = (hazelnut::IvshHeader*)&ptr[0]; - pFrameHeader = (hazelnut::FrameHeader*)&ptr[0x1000]; - - sessionId = pHeader->serverSessionId.load(); - lastFrameSerial = 0; - return true; - } - - // NOTE: This function leaves the lock held on successful returns. - ResultCode TickOne() { - if(!pHeader) - return ResultCode::Fail; - - if(sessionId != pHeader->serverSessionId.load()) - return ResultCode::Fail; - - if(pHeader->lock.try_lock_manually()) { - // failed to lock - return ResultCode::LockContended; - } - - auto current = pFrameHeader->serial.load(); - if(current == lastFrameSerial) { - return ResultCode::Unchanged; - } - - lastFrameSerial = current; - return ResultCode::Changed; - } - - void Lock() { pHeader->lock.lock_manually(); } - - void Unlock() { pHeader->lock.unlock(); } - - u32* Framebuffer() { - if(pFrameHeader) { - return pFrameHeader->bits(); - } - - return nullptr; - } - - hazelnut::IvshmemDevice ivshmemDevice; - - // Protocol/memory stuff - hazelnut::IvshHeader* pHeader; - hazelnut::FrameHeader* pFrameHeader; - - u32 sessionId {}; - - u32 lastFrameSerial {}; -}; - -// Rust bindings +// Rust bindings to the C++ Hazelnut client library extern "C" { void* rust_new_hazelnut_client() { - return (void*)new HazelnutIvshmemClient; + return (void*)new hazelnut::Client; } void rust_destroy_hazelnut_client(void* pClient) { if(pClient) - delete(HazelnutIvshmemClient*)pClient; + delete(hazelnut::Client*)pClient; } bool rust_hazelnut_client_open(void* pClient, int fd) { if(pClient) { - return ((HazelnutIvshmemClient*)pClient)->Open(fd); + return ((hazelnut::Client*)pClient)->Open(fd); } return false; @@ -86,41 +22,41 @@ bool rust_hazelnut_client_open(void* pClient, int fd) { int rust_hazelnut_client_tick(void* pClient) { if(pClient) { - return (int)((HazelnutIvshmemClient*)pClient)->TickOne(); + return (int)((hazelnut::Client*)pClient)->TickOne(); } - return (int)ResultCode::Fail; + return (int)hazelnut::Client::ResultCode::Fail; } void rust_hazelnut_client_lock(void* pClient) { if(pClient) { - return ((HazelnutIvshmemClient*)pClient)->Lock(); + return ((hazelnut::Client*)pClient)->Lock(); } } void rust_hazelnut_client_unlock(void* pClient) { if(pClient) { - return ((HazelnutIvshmemClient*)pClient)->Unlock(); + return ((hazelnut::Client*)pClient)->Unlock(); } } u32* rust_hazelnut_client_get_framebuffer(void* pClient) { if(pClient) { - return ((HazelnutIvshmemClient*)pClient)->Framebuffer(); + return ((hazelnut::Client*)pClient)->Framebuffer(); } return nullptr; } u32 rust_hazelnut_client_get_width(void* pClient) { - if(pClient) { - return ((HazelnutIvshmemClient*)pClient)->pFrameHeader->width.load(); + if(pClient) { + return ((hazelnut::Client*)pClient)->GetWidth(); } - return 0; + return 0; } u32 rust_hazelnut_client_get_height(void* pClient) { - if(pClient) { - return ((HazelnutIvshmemClient*)pClient)->pFrameHeader->height.load(); + if(pClient) { + return ((hazelnut::Client*)pClient)->GetHeight(); } - return 0; + return 0; } } \ No newline at end of file diff --git a/shared/README.md b/shared/README.md index f0b71f7..3c8f272 100644 --- a/shared/README.md +++ b/shared/README.md @@ -2,4 +2,6 @@ This is all of the things shared between the agent and client ends. -It is cross platform C++20. \ No newline at end of file +It is cross platform C++20. + +(Some of this should be moved to a client-cpp directory, probably.) \ No newline at end of file diff --git a/shared/src/client.cpp b/shared/src/client.cpp new file mode 100644 index 0000000..61a4dd9 --- /dev/null +++ b/shared/src/client.cpp @@ -0,0 +1,78 @@ +#include "client.hpp" + +#include "atomic_spinlock.hpp" +#include "ivshmem_protocol.hpp" +#include "Utils.hpp" + +namespace hazelnut { + + bool Client::Open(int fd) { + // We probably should have this return a tuple of + // so that we can pass the errno to Rust in a semi-sane manner, + // and actually communicate that. However, Rust/the code opening the file + // will handle file open errors which is probably the brink of most of the errors possible. + if(!ivshmemDevice.Open(fd)) + return false; + + auto* ptr = (u8*)ivshmemDevice.GetPointer(); + + pHeader = (hazelnut::IvshHeader*)&ptr[0]; + pFrameHeader = (hazelnut::FrameHeader*)&ptr[0x1000]; + + sessionId = pHeader->serverSessionId.load(); + lastFrameSerial = 0; + return true; + } + + Client::ResultCode Client::TickOne() { + if(!pHeader) + return ResultCode::Fail; + + if(sessionId != pHeader->serverSessionId.load()) + return ResultCode::Fail; + + if(pHeader->lock.try_lock_manually()) { + // failed to lock + return ResultCode::LockContended; + } + + auto current = pFrameHeader->serial.load(); + if(current == lastFrameSerial) { + return ResultCode::Unchanged; + } + + lastFrameSerial = current; + return ResultCode::Changed; + } + + void Client::Lock() { + pHeader->lock.lock_manually(); + } + + void Client::Unlock() { + pHeader->lock.unlock(); + } + + u32* Client::Framebuffer() { + if(pFrameHeader) { + return pFrameHeader->bits(); + } + + return nullptr; + } + + u32 Client::GetWidth() { + if(pFrameHeader) + return pFrameHeader->width.load(); + + return -1; + } + + u32 Client::GetHeight() { + if(pFrameHeader) + return pFrameHeader->width.load(); + + return -1; + } + +} // namespace hazelnut \ No newline at end of file diff --git a/shared/src/client.hpp b/shared/src/client.hpp new file mode 100644 index 0000000..ba40173 --- /dev/null +++ b/shared/src/client.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "ivshmem.hpp" + +namespace hazelnut { + struct IvshHeader; + struct FrameHeader; + + struct Client { + enum class ResultCode : u32 { Unchanged, Changed, LockContended, Fail }; + + bool Open(int fd); + + // NOTE: This function leaves the lock held on successful returns. + ResultCode TickOne(); + + void Lock(); + + void Unlock(); + + // Only call these methods when the lock is held. + + u32* Framebuffer(); + + u32 GetWidth(); + + u32 GetHeight(); + + private: + hazelnut::IvshmemDevice ivshmemDevice; + + // Protocol/memory stuff + hazelnut::IvshHeader* pHeader; + hazelnut::FrameHeader* pFrameHeader; + + u32 sessionId {}; + + u32 lastFrameSerial {}; + }; + +} // namespace hazelnut diff --git a/shared/src/ivshmem_protocol.hpp b/shared/src/ivshmem_protocol.hpp index a7ea519..63fe490 100644 --- a/shared/src/ivshmem_protocol.hpp +++ b/shared/src/ivshmem_protocol.hpp @@ -30,6 +30,10 @@ namespace hazelnut { std::atomic width{}; std::atomic height{}; + // FIXME: We should have a frame format, with the following possible types + // RGBA - The current RGBA backed framebuffefr + // H264 - NVENC H.264 (we should probably also have a flag for frame type as well) + // obtain bits at the next page boundary u32* bits() { return reinterpret_cast(reinterpret_cast(this) + 0x1000);