proxy: fix BuildList to use non-const iteration for interface types #304

pull ryanofsky wants to merge 1 commits into bitcoin-core:master from ryanofsky:pr/buildlist changing 7 files +67 −3
  1. ryanofsky commented at 6:30 PM on July 8, 2026: collaborator

    Needed for bitcoin/bitcoin#10102 since #277 was merged.

    Since #277, it is no longer possible to return vector<unique_ptr<ExternalSigner>> from Node::listExternalSigners() in bitcoin/bitcoin#10102, because the proxy server objects libmultiprocess creates to wrap the ExternalSigner objects need to take ownership of the objects to keep them alive, so they need to be moved out of the returned vector, which can only be done if BuildField is passed a non-const vector element.

    This PR restores previous behavior before #277 making it possible to mutate container elements while serializing them, in cases like this where it is necessary.

  2. DrahtBot commented at 6:30 PM on July 8, 2026: none

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process. A summary of reviews will appear here.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #332 (refactor: drop redundant semicolons by fanquake)
    • #298 (Fix error handling when creating clients (mp::ConnectStream) by xyzconstant)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. ryanofsky force-pushed on Jul 8, 2026
  4. ryanofsky commented at 7:19 PM on July 8, 2026: collaborator

    <!-- begin push-3 -->

    Updated a0b01d73698597293e48771cad2b4cb8e0ba16a0 -> 46deb5e6bcb853c29f7573ce5e19414929f7f551 (pr/buildlist.2 -> pr/buildlist.3, compare)<!-- end --> to fix IWYU CI failures: add #include <mp/proxy-io.h> to generated proxy-client files and reserve() call in listCallbacks https://github.com/bitcoin-core/libmultiprocess/actions/runs/28966350534/job/85950397301<!-- end -->

  5. in test/mp/test/test.cpp:269 in 46deb5e6bc
     264 | +    // so unique_ptr::release() can transfer ownership to the proxy server.
     265 | +    std::vector<std::unique_ptr<FooCallback>> callbacks{foo->listCallbacks(3)};
     266 | +    KJ_REQUIRE(callbacks.size() == 3u);
     267 | +    for (int i = 0; i < 3; ++i) {
     268 | +        KJ_REQUIRE(callbacks[i] != nullptr);
     269 | +        KJ_EXPECT(callbacks[i]->call(0) == i);
    


    Sjors commented at 1:01 PM on July 9, 2026:

    IIUC this isn't really a callback flow, so the terminology had me confused about the test. Might be better to add another (Bar) interface to make it more clear.

    diff --git a/test/mp/test/foo-types.h b/test/mp/test/foo-types.h
    index b96eabfcfb..96506a9f50 100644
    --- a/test/mp/test/foo-types.h
    +++ b/test/mp/test/foo-types.h
    @@ -40,4 +40,5 @@ struct FooCallback; // IWYU pragma: export
     struct FooFn; // IWYU pragma: export
     struct FooInterface; // IWYU pragma: export
    +struct BarInterface; // IWYU pragma: export
     } // namespace messages
    
    diff --git a/test/mp/test/foo.capnp b/test/mp/test/foo.capnp
    index edf7841c8a..0896274209 100644
    --- a/test/mp/test/foo.capnp
    +++ b/test/mp/test/foo.capnp
    @@ -37,5 +37,5 @@ interface FooInterface $Proxy.wrap("mp::test::FooImplementation") {
         callIntFnAsync [@21](/bitcoin-core-multiprocess/contributor/21/) (context :Proxy.Context, arg :Int32) -> (result :Int32);
         passDataPointers [@22](/bitcoin-core-multiprocess/contributor/22/) (arg :List(Data)) -> (result :List(Data));
    -    listCallbacks [@24](/bitcoin-core-multiprocess/contributor/24/) (context :Proxy.Context, n :Int32) -> (result :List(FooCallback));
    +    listBars [@24](/bitcoin-core-multiprocess/contributor/24/) (context :Proxy.Context, n :Int32) -> (result :List(BarInterface));
     }
    
    @@ -49,4 +49,9 @@ interface ExtendedCallback extends(FooCallback) $Proxy.wrap("mp::test::ExtendedC
     }
    
    +interface BarInterface $Proxy.wrap("mp::test::Bar") {
    +    destroy [@0](/bitcoin-core-multiprocess/contributor/0/) (context :Proxy.Context) -> ();
    +    value [@1](/bitcoin-core-multiprocess/contributor/1/) (context :Proxy.Context) -> (result :Int32);
    +}
    +
     interface FooFn $Proxy.wrap("ProxyCallback<std::function<int()>>") {
         destroy [@0](/bitcoin-core-multiprocess/contributor/0/) (context :Proxy.Context) -> ();
    diff --git a/test/mp/test/foo.h b/test/mp/test/foo.h
    index 17aeeb8388..5c881117ce 100644
    --- a/test/mp/test/foo.h
    +++ b/test/mp/test/foo.h
    @@ -60,13 +60,4 @@ public:
     };
    
    -//! Concrete FooCallback that returns a fixed value, used by listCallbacks tests.
    -class SimpleCallback : public FooCallback
    -{
    -public:
    -    explicit SimpleCallback(int value) : m_value(value) {}
    -    int call(int) override { return m_value; }
    -    int m_value;
    -};
    -
     class ExtendedCallback : public FooCallback
     {
    @@ -75,4 +66,27 @@ public:
     };
    
    +//! A second, arbitrary interface used to test returning a
    +//! list of interface objects.
    +class Bar
    +{
    +public:
    +    virtual ~Bar() = default;
    +    virtual int value() = 0;
    +};
    +
    +//! Concrete Bar that returns a fixed value, used by listBars tests.
    +class SimpleBar : public Bar
    +{
    +public:
    +    explicit SimpleBar(int value) : m_value(value) {}
    +    int value() override { return m_value; }
    +    int m_value;
    +};
    +
     class FooImplementation
     {
    @@ -99,9 +113,9 @@ public:
         int passFn(std::function<int()> fn) { return fn(); }
         std::vector<FooDataRef> passDataPointers(std::vector<FooDataRef> values) { return values; }
    -    std::vector<std::unique_ptr<FooCallback>> listCallbacks(int n)
    +    std::vector<std::unique_ptr<Bar>> listBars(int n)
         {
    -        std::vector<std::unique_ptr<FooCallback>> result;
    +        std::vector<std::unique_ptr<Bar>> result;
             result.reserve(n);
    -        for (int i = 0; i < n; ++i) result.push_back(std::make_unique<SimpleCallback>(i));
    +        for (int i = 0; i < n; ++i) result.push_back(std::make_unique<SimpleBar>(i));
             return result;
         }
    diff --git a/test/mp/test/test.cpp b/test/mp/test/test.cpp
    index 41ca506c62..41aa4d67fd 100644
    --- a/test/mp/test/test.cpp
    +++ b/test/mp/test/test.cpp
    @@ -263,9 +263,9 @@ KJ_TEST("Call FooInterface methods")
         // BuildList with interface element types, which requires non-const iteration
         // so unique_ptr::release() can transfer ownership to the proxy server.
    -    std::vector<std::unique_ptr<FooCallback>> callbacks{foo->listCallbacks(3)};
    -    KJ_REQUIRE(callbacks.size() == 3u);
    +    std::vector<std::unique_ptr<Bar>> bars{foo->listBars(3)};
    +    KJ_REQUIRE(bars.size() == 3u);
         for (int i = 0; i < 3; ++i) {
    -        KJ_REQUIRE(callbacks[i] != nullptr);
    -        KJ_EXPECT(callbacks[i]->call(0) == i);
    +        KJ_REQUIRE(bars[i] != nullptr);
    +        KJ_EXPECT(bars[i]->value() == i);
         }
     }
    

    ryanofsky commented at 1:54 PM on July 9, 2026:

    re: #304 (review)

    IIUC this isn't really a callback flow, so the terminology had me confused about the test. Might be better to add another (Bar) interface to make it more clear.

    Yeah you are right. The server is returning callback objects in the sense of "you can call me back later by using these objects" not taking callback objects that it can use to call into the client later. So your example makes more sense and its better to avoid the callback term here.


    ryanofsky commented at 1:40 AM on July 30, 2026:

    re: #304 (review)

    Applied suggested changes. I tried to do something a simpler adding a FooInterface method that returns a list of FooInterfaces, but it didn't work out because there's no C++ FooInterface class with virtual methods. It might make sense to add virtual methods and have separate FooInterface / FooImplementation classes which would make the test a little more realistic. But it would also require defining each method twice and overriding it, so seem better to keep current test setup.

  6. in include/mp/proxy-types.h:288 in 46deb5e6bc outdated
     284 | @@ -285,7 +285,7 @@ void BuildList(TypeList<LocalType>, InvokeContext& invoke_context, Output&& outp
     285 |  {
     286 |      auto list = output.init(value.size());
     287 |      size_t i = 0;
     288 | -    for (const auto& elem : value) {
     289 | +    for (auto&& elem : value) {
    


    ryanofsky commented at 1:47 PM on July 9, 2026:

    In commit "proxy: fix BuildList to use non-const iteration for interface types" (a0b01d73698597293e48771cad2b4cb8e0ba16a0)

    Note, while this change mostly restores behavior to what it was before #277, passing elem to BuildField below as an lvalue reference regardless of whether the container is lvalue or rvalue, it might make more sense to pass elem as an rvalue if the container is an rvalue, or as an lvalue if it's an lvalue with something like:

    for (auto&& elem : value) {
        if constexpr (std::is_lvalue_reference_v<Value&&>) {
            BuildField(/*...*/, elem);
        } else {
            BuildField(/*...*/, std::move(elem));
        }
    }
    

    This could be safer because it could allow BuildField to distinguish based on the way the container is being used and only move from temporary containers that are about to be destroyed, not containers passed as references.

    Also it might be better to replace for (auto&& elem : value) with for (auto it = elem.begin(); it != elem.end(); ++it) like the previous code had since this would could also preserve rvaluedness of dereferencing the iterator, if the iterator deferences as a proxy object instead of a reference like vector<bool>`

    Which is just to say that a0b01d73698597293e48771cad2b4cb8e0ba16a0 is a minimal change restoring ability to return lists of interface pointers, but it might make sense to actually increase safety here as well.


    ryanofsky commented at 2:12 AM on July 30, 2026:

    re: #304 (review)

    Latest push implements all these suggestions so rvaluedness is preserved and the code should be safer and potentially work more types. It is a little more verbose and complicated now, but I think not too bad.

  7. ryanofsky commented at 1:55 PM on July 9, 2026: collaborator

    Thanks for review. Looks like there are some things that should be improved here so I will make this a draft

  8. ryanofsky marked this as a draft on Jul 9, 2026
  9. proxy: fix BuildList to use non-const iteration for interface types
    BuildList turns a C++ container into a capnp list. It looped over the
    container with `for (const auto& elem : value)`, so every element was
    const. That breaks when the elements are interface pointers: to send one
    to the capnp server the proxy code has to take ownership away from the
    unique_ptr by calling unique_ptr::release(), and that cannot be called on
    a const object. So the code failed to compile, and returning a list of
    interface pointers stopped working after #277.
    
    Fix this by looping with a plain iterator and passing each element to
    BuildField as non-const, only moving elements out of the container when
    the container itself is a temporary. See the code comment for details.
    
    In Bitcoin Core, interfaces::Node::listExternalSigners() returns a
    vector<unique_ptr<ExternalSigner>> over IPC and hits this code path.
    
    Add a `listBars` method to the test FooInterface that returns a
    vector<unique_ptr<Bar>>, plus a test that calls it over IPC, so this is
    covered and does not break again.
    
    Co-Authored-by: Sjors Provoost <sjors@sprovoost.nl>
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    f9e1c8d6b5
  10. ryanofsky force-pushed on Jul 30, 2026
  11. ryanofsky commented at 1:42 AM on July 30, 2026: collaborator

    <!-- begin push-4 -->

    Updated 46deb5e6bcb853c29f7573ce5e19414929f7f551 -> b14b0bfaeaeffc9115af0bfc12490b233b8bb33a (pr/buildlist.3 -> pr/buildlist.4, compare)<!-- end --> updating test as suggested

    <!-- begin push-5 -->

    Rebased b14b0bfaeaeffc9115af0bfc12490b233b8bb33a -> 5b13fe52d6a9ce1b47b20e5fafbcfe0c77db2d86 (pr/buildlist.4 -> pr/buildlist.5, compare)<!-- end --> due to silent conflict with #305 and adding more rvalueness preserving code for more safety (to avoid moving from lvalues)

    <!-- begin push-6 -->

    Updated 5b13fe52d6a9ce1b47b20e5fafbcfe0c77db2d86 -> f9e1c8d6b518888dceb2f1b4f609f3004ff73554 (pr/buildlist.5 -> pr/buildlist.6, compare)<!-- end --> to fix IWYU error https://github.com/bitcoin-core/libmultiprocess/actions/runs/30508160651/job/90762305561?pr=304

  12. ryanofsky force-pushed on Jul 30, 2026
  13. ryanofsky force-pushed on Jul 30, 2026
  14. ryanofsky marked this as ready for review on Jul 30, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/libmultiprocess. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-05 20:30 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me