cmUVHandlePtr: Add uv_write wrapper to manage request lifetime

Provide a way to synchronously cancel a write request callback.
This commit is contained in:
Brad King
2023-10-31 10:11:20 -04:00
parent bec0dd93a3
commit 7ee5fb01c6
3 changed files with 112 additions and 0 deletions

View File

@@ -6,6 +6,9 @@
#include <cassert>
#include <cstdlib>
#include <mutex>
#include <utility>
#include <cm/memory>
#include <cm3p/uv.h>
@@ -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<std::function<void(int)>> cb_;
write_req(std::weak_ptr<std::function<void(int)>> 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<write_req> self(static_cast<write_req*>(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<std::function<void(int)>> cb)
{
auto req = cm::make_unique<write_req>(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<void>(req.release());
}
return status;
}
}

View File

@@ -5,6 +5,7 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <type_traits>
@@ -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<std::function<void(int)>> cb);
}

View File

@@ -1,7 +1,10 @@
#include <functional>
#include <iostream>
#include <memory>
#include <cm3p/uv.h>
#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<std::function<void(int)>>(
[&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;
}