From 7ee5fb01c62952c3e8b10afb646be772a6eb47af Mon Sep 17 00:00:00 2001 From: Brad King Date: Tue, 31 Oct 2023 10:11:20 -0400 Subject: cmUVHandlePtr: Add uv_write wrapper to manage request lifetime Provide a way to synchronously cancel a write request callback. --- Source/cmUVHandlePtr.cxx | 37 +++++++++++++++++++++++++++ Source/cmUVHandlePtr.h | 24 ++++++++++++++++++ Tests/CMakeLib/testUVHandlePtr.cxx | 51 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/Source/cmUVHandlePtr.cxx b/Source/cmUVHandlePtr.cxx index 7d41644..168f1a6 100644 --- a/Source/cmUVHandlePtr.cxx +++ b/Source/cmUVHandlePtr.cxx @@ -6,6 +6,9 @@ #include #include #include +#include + +#include #include @@ -306,4 +309,38 @@ UV_HANDLE_PTR_INSTANTIATE_EXPLICIT(async) UV_HANDLE_PTR_INSTANTIATE_EXPLICIT(tty) #endif + +namespace { +struct write_req : public uv_write_t +{ + std::weak_ptr> cb_; + write_req(std::weak_ptr> wcb) + : cb_(std::move(wcb)) + { + } +}; + +void write_req_cb(uv_write_t* req, int status) +{ + // Ownership has been transferred from the event loop. + std::unique_ptr self(static_cast(req)); + + // Notify the original uv_write caller if it is still interested. + if (auto cb = self->cb_.lock()) { + (*cb)(status); + } +} +} + +int uv_write(uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, + std::weak_ptr> cb) +{ + auto req = cm::make_unique(std::move(cb)); + int status = uv_write(req.get(), handle, bufs, nbufs, write_req_cb); + if (status == 0) { + // Ownership has been transferred to the event loop. + static_cast(req.release()); + } + return status; +} } diff --git a/Source/cmUVHandlePtr.h b/Source/cmUVHandlePtr.h index 174dc45..1b5eb9c 100644 --- a/Source/cmUVHandlePtr.h +++ b/Source/cmUVHandlePtr.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -286,4 +287,27 @@ UV_HANDLE_PTR_INSTANTIATE_EXTERN(tty) # undef UV_HANDLE_PTR_INSTANTIATE_EXTERN #endif + +/** + * Wraps uv_write to add synchronous cancellation. + * + * libuv provides no way to synchronously cancel a write request. + * Closing a write handle will cancel its pending write request, but its + * callback will still be called asynchronously later with UV_ECANCELED. + * + * This wrapper provides a solution by handing ownership of the uv_write_t + * request object to the event loop and taking it back in the callback. + * Use this in combination with uv_loop_ptr to ensure the event loop + * runs to completion and cleans up all resources. + * + * The caller may optionally provide a callback it owns with std::shared_ptr. + * If the caller's lifetime ends before the write request completes, the + * callback can be safely deleted and will not be called. + * + * The bufs array does not need to live beyond this call, but the memory + * referenced by the uv_buf_t values must remain alive until the callback + * is made or the stream is closed. + */ +int uv_write(uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, + std::weak_ptr> cb); } diff --git a/Tests/CMakeLib/testUVHandlePtr.cxx b/Tests/CMakeLib/testUVHandlePtr.cxx index 3c070b0..c97d755 100644 --- a/Tests/CMakeLib/testUVHandlePtr.cxx +++ b/Tests/CMakeLib/testUVHandlePtr.cxx @@ -1,7 +1,10 @@ +#include #include +#include #include +#include "cmGetPipes.h" #include "cmUVHandlePtr.h" static bool testIdle() @@ -77,10 +80,58 @@ static bool testTimer() return true; } +static bool testWriteCallback() +{ + int pipe[] = { -1, -1 }; + if (cmGetPipes(pipe) < 0) { + std::cout << "cmGetPipes() returned an error" << std::endl; + return false; + } + + cm::uv_loop_ptr loop; + loop.init(); + + cm::uv_pipe_ptr pipeRead; + pipeRead.init(*loop, 0); + uv_pipe_open(pipeRead, pipe[0]); + + cm::uv_pipe_ptr pipeWrite; + pipeWrite.init(*loop, 0); + uv_pipe_open(pipeWrite, pipe[1]); + + char c = '.'; + uv_buf_t buf = uv_buf_init(&c, sizeof(c)); + int status = -1; + auto cb = std::make_shared>( + [&status](int s) { status = s; }); + + // Test getting a callback after the write is done. + cm::uv_write(pipeWrite, &buf, 1, cb); + uv_run(loop, UV_RUN_DEFAULT); + if (status != 0) { + std::cout << "cm::uv_write non-zero status: " << status << std::endl; + return false; + } + + // Test deleting the callback before it is made. + status = -1; + cm::uv_write(pipeWrite, &buf, 1, cb); + cb.reset(); + uv_run(loop, UV_RUN_DEFAULT); + if (status != -1) { + std::cout << "cm::uv_write callback incorrectly called with status: " + << status << std::endl; + return false; + } + + return true; +} + int testUVHandlePtr(int, char** const) { bool passed = true; passed = testIdle() && passed; passed = testTimer() && passed; + passed = testWriteCallback() && passed; return passed ? 0 : -1; } -- cgit v0.12