chriskohlhoff / asio

Asio C++ Library
http://think-async.com/Asio
4.86k stars 1.21k forks source link

Polymorphic SSL stream handling doesn't seem possible... #234

Open akoolenbourke opened 7 years ago

akoolenbourke commented 7 years ago

I am looking to add SSL/TLS to our ASIO code and the obvious first choice is to use asio::ssl::stream. However I noticed that ssl::stream and tcp::socket share no common base. This makes it difficult to build an object that has a pointer to some "stream" object that dispatches according to SSL or not. Whilst a think wrapper can be created to emulate this it obviously fails around the member function templates for async_read_some etc due to the lack of multiple dispatch in C++. We also need the ability to layer multiple streams (SSL, plain or our own filters) into this layered system so we can't guarantee a fixed set of types.

We were hoping to build a system where we can dynamically layer streams on top of each other and still have all the benefits of ASIO (Many buffer types, CompletionToken support etc)

Is there any recommended solution or do we have to end up with a solution involving concrete types?

vinniefalco commented 7 years ago

This topic seems mighty familiar :)

akoolenbourke commented 7 years ago

It does! I posted in both repos about the same time. However I've since moved onto my next headache - generic async friendly pipelines :) I was going to post on the ASIO mailing list but I'll ask you first while I have you. Do you know if there's a code anywhere for coroutines TS support in ASIO? I could look to writing my own but I have some vague recollection there is an implementation out there but I've been unable to find it again. Even if I have to adjust it for the networking TS that's OK.

Cheers

vinniefalco commented 7 years ago

This is just a guess but I think you need to be using the Asio coroutines, not the Coroutines TS ones, otherwise you lose the guarantees that your I/O objects will be accessed safely. Specifically, that coroutines for I/O objects will 1. only be called from a thread that is currently calling io_service::run, and 2. be automatically invoked through an explicit strand.

vinniefalco commented 7 years ago

Side note, I have rewritten a lot of Beast's composed operations to use stackless coroutines. They are a little weird but damn useful. And their performance characteristics are outstanding, you get the best of coroutines with the resource consumption profile of callbacks.

akoolenbourke commented 7 years ago

I haven't looked into doing an implementation too deeply but I thought that given the Universal Async Model, one could write the appropriate Awaiter and associated invoke hooks etc to get things to work.

The code I thought I had seen, if my memory serves, was from Chris himself somewhere but I'm just real foggy on the whole thing.

Are the stackless coroutines in Beast available in the repo? If so I might take a look. I've avoided them due to their narrow signature requirements (All steps need to have the same signature IIRC) which doesn't work for doing something so you can resolve, connect, read, write in one function.

I've been currently looking at implementing a generic pipeline of function objects and also looking at the "c++ 14 stackless coroutines" github which is really a pipeline too. Those have some advantages (Building pipelines easily and reusing code) but I do LOVE the idea of using coroutines.....it's just a matter of finding a good stackless implementation.

I've been stalling for weeks trying to nut out some of this pipeline/coroutine stuff and I'm going insane.

vinniefalco commented 7 years ago

I thought that given the Universal Async Model, one could write the appropriate Awaiter and associated invoke hooks etc to get things to work.

They still have to run on the io_service. So what would be the benefit over using the stackful coroutines already built-in to Asio?

Chris wrote the stackless coroutine code, yes.

Are the stackless coroutines in Beast available in the repo?

Yes. Note they are part of the implementation, not the interface:

Simple: https://github.com/boostorg/beast/blob/d6fce5a00fd29862fde58abc15f2bbc33fe13ce4/include/boost/beast/websocket/impl/ping.ipp#L122

Complex: https://github.com/boostorg/beast/blob/d6fce5a00fd29862fde58abc15f2bbc33fe13ce4/include/boost/beast/websocket/impl/read.ipp#L122

I've avoided them due to their narrow signature requirements (All steps need to have the same signature IIRC) which doesn't work for doing something so you can resolve, connect, read, write in one function.

It is true that you can't resolve, because the ResolveHandler second parameter is a resolver::iterator but what's the big deal? Just make the resolve step separate. It is worth it for what you get from being able to write your code linearly. For the rest of the functions just use default arguments:

void op::operator()(
    boost::system::error_code ec = {},
    std::size_t bytes_transferred = 0);

This works with everything except async_resolve.

it's just a matter of finding a good stackless implementation.

Disclaimer: This is just a guess on my part.... BUT....

I think you are going to experience pain if you attempt to utilize external coroutine facilities and expect them to work correctly with Asio. The coroutine support built-in to Asio is first class, why avoid it?

Beast provides a generous number of examples of asio coroutine usage, both stackful and stackless. I would stick to what works: http://www.boost.org/doc/libs/develop/libs/beast/doc/html/beast/examples.html

akoolenbourke commented 7 years ago

They still have to run on the io_service. So what would be the benefit over using the stackful coroutines already built-in to Asio?

I assume you mean stackless? If stackful then it's memory. I could be running many thousands of these co-routines at any time.

Chris wrote the stackless coroutine code, yes.

When I mentioned Chris is was in regard to the coroutine TS implementation that I have some vague memory of seeing a long time ago.

It is true that you can't resolve, because the ResolveHandler second parameter is a resolver::iterator but what's the big deal? Just make the resolve step separate. It is worth it for what you get from being able to write your code linearly. For the rest of the functions just use default arguments:

async_connect doesn't work either and as I write my own composed operations I could see more things becoming incompatible - but I do need to to more experimentation.

Anyway, I'm not convinced coroutines is best for what we want but it might enable me to make progress as I've stalled for weeks trying to solve the pipeline problem. We want to build a set of networking building blocks, a level or so higher in abstraction to ASIO, and then easily build pipelines of these objects at will and run them. Something along the lines of Facebook's Wangle, but that thing looked like a mess - especially seeing as I build on Windows.

Disclaimer: This is just a guess on my part.... BUT.... I think you are going to experience pain if you attempt to utilize external coroutine facilities and expect them to work correctly with Asio. The coroutine support in Asio is first class. Beast provides a generous number of examples of their use. I would stick to what works: http://www.boost.org/doc/libs/develop/libs/beast/doc/html/beast/examples.html

You could be right, but researching the proposed coroutines TS and thinking about async operations in ASIO I have a feeling it could be fine.

vinniefalco commented 7 years ago

async_connect doesn't work either

Of course it does. ConnectHandler signature is void(error_code). http://www.boost.org/doc/libs/1_65_0/doc/html/boost_asio/reference/ConnectHandler.html

You can use this signature for your completion handler:

void op::operator()(
    boost::system::error_code ec = {},
    std::size_t bytes_transferred = 0);

Where op is the name of your composed operation class. bytes_transferred will be zero on the completion of a call to async_connect.

vinniefalco commented 7 years ago

Anyway, I'm not convinced coroutines is best for what we want but it might enable me to make progress as I've stalled for weeks trying to solve the pipeline problem.

The questions of whether to use stackful coroutines in calling code and how to arrange a class hierarchy for pipelining operations are orthogonal.

Operation pipelines are built canonically out of composed operations. These operations must use traditional completion handlers (callbacks) or else they will be clumsy to compose. The stackless coroutines are really just a clever use of completion handlers.

I don't see how you could build a sane composed operation using a stackful coroutine. Would you spawn the coroutine every time the initiating function is called? That doesn't make sense at all. Nor does requiring the caller to always pass in a yield_context.

chriskohlhoff commented 7 years ago

On Fri, Sep 8, 2017, at 08:43 AM, akoolenbourke wrote:

When I mentioned Chris is was in regard to the coroutine TS implementation that I have some vague memory of seeing a long time ago. Please see the co_await branch and WG21 paper P0286r0. The branch probably needs fixing to work with latest visual studio.

vinniefalco commented 7 years ago

P0286r0

Beautiful :) However, @akoolenbourke before you get too excited note that none of the examples in P0286r0 demonstrate the implementation of initiating functions using co_await.

akoolenbourke commented 7 years ago

Of course it does. ConnectHandler signature is void(error_code).

That will teach me. I mentioned it as I had done a quick google, and the non member async_connect popped up which returns an iterator in the handler.

I don't see how you could build a sane composed operation using a stackful coroutine. Would you spawn the coroutine every time the initiating function is called? That doesn't make sense at all. Nor does requiring the caller to always pass in a yield_context.

I think there's some confusion. I don't think I ever mentioned stackful coroutines - I'd be looking at stackless. Also, I wouldn't use coroutines in a composed operation. I'd use them in client code.

void start_some_pipeline(context &c)
{
    async_do_this(blah, c);
    check errors, do some logic

    async_do_that(blah,c)
    check errors, do some logic

    async_do_the_other(blah,c)
    check errors, do some logic
}

Please see the co_await branch and WG21 paper P0286r0. The branch probably needs fixing to work with latest visual studio.

Thanks @chriskohlhoff, that looks like what I was talking about.

Beautiful :) However, @akoolenbourke before you get too excited note that none of the examples in P0286r0 demonstrate the implementation of initiating functions using co_await.

That's fine, I wasn't going to do that anyway :)

vinniefalco commented 7 years ago

Also, I wouldn't use coroutines in a composed operation. I'd use them in client code.

You wrote:

We want to build a set of networking building blocks, a level or so higher in abstraction to ASIO, and then easily build pipelines of these objects at will and run them.

I read "network building blocks" as "collection of domain-specific asynchronous initiating functions", which means you will be writing composed operations. That is how higher level abstractions are built in Asio.

A composed operation is like a subroutine. You call it with parameters and a completion handler, and it goes and does its thing. When it is done, it calls your completion handler (the "upcall"). This upcall is the equivalent of "return" from your subroutine. It is called "composed" because the implementation of the operation can consist of calls to other initiating functions which themselves launch composed operations. For example, boost::asio::async_write is a composed operation which uses the async_write_some initiating function of an AsyncWriteStream.

If you plan to write all your code using stackless coroutines but without implementing any composed operations, then you haven't really built any new high level abstractions. You have no mechanism for transferring the flow of control to the caller (the code which launched the initiating function). You can't "return." The implication is that your code will be a single linear flow of control.

Given that you want to said you want "build a set of networking building blocks" I rather doubt that this is what you intended. Perhaps if could provide some specifics about the operations you'd like to perform, it could be more clear. Or maybe I simply misunderstood you.

There is one alternative to composed operations and that is to use stackful coroutines. Now, a simple function whose signature includes 1. an I/O object (like a socket) and 2. a value of type basic_yield_context<...> can function in a role similar to composed operations which use completion handlers. However, you said you ruled out stackful coroutines. Furthermore, an implementation which mandated coroutines for all intermediate high level operations would consume more resources than one which used completion handlers.

akoolenbourke commented 7 years ago

By "building blocks" I meant a collection of "things" that would encapsulate domain-specific requirements as you said but not necessarily initiating functions/composed operations. For instance maybe they'd be a collection of function objects stored in a pipeline that knows how to run that pipeline.

So in that sense, yes we're talking about different things so that's the confusion.

In detail:

I'm just investigating the ways to handle continuation whilst encapsulating functionality that I can build up in any order I want for various services, protocols and what not. I am not a fan of callback-spaghetti, nor in building logic/state switching for "pipeline" progression into the actual handlers/callbacks of the pipeline operations. Our legacy (Not async) code does that and maintenance is troublesome.

This is why I've come to look at co_routines now. It seems (I am new to them) that I don't have to build complex error prone machinery for a generic, async-friendly pipeline (I've spent about a week attempting it and I'm not happy). Hopefully I can effectively code my pipeline in-place, without spaghetti. Then if I need a different one I can have another spawned function that is a different "pipeline" made up of whatever functions (Initiating or not) that that needs, but reusing code as much as possible.

The only thing I was not sure of (because I have never used co-routines) is if I had to end up copying and pasting a lot of support code (Error handling etc) around initiating/custom functions. However reading the paper linked by Chris in his "Refactoring" example shows it's trivial to break it out into encapsulated functionality.

Thanks again for the help.

BTW I have the coroutines TS from the co_await branch building now (VS2017) and - seemingly - working (with timers) but I won't celebrate just yet. Hopefully this will prove a success as it does look very exciting.

akoolenbourke commented 7 years ago

OK I spoke a bit early. When using ASIO_NO_DEPRECATED which I have to, things break. My knowledge of ASIO just isn't there to fix this now, but I might revisit it later. If I get it working I'm happy to offer up the changes if it's of any value.

Out of interest, here is the error that has me stumped. (Note this is the only error line VS gives so I can't follow a chain to find a real issue)

error C3312: no callable 'await_resume' function found for type 'asio::async_result<asio::basic_unsynchronized_await_context<asio::strand<asio::executor>>,void (asio::error_code)>::return_type

return_type is

    typedef typename detail::await_handler<
        Executor, typename decay<Args>::type...>::awaitable_type return_type;

and awaitable_type of await_handler does have an await_resume from what I can see. I'll note I am using a frankenbuild. A select few files from the co_await branch in addition to the networking TS ASIO.

@chriskohlhoff Is there any intention of modernising the support for co_await etc?

cstratopoulos commented 6 years ago

@akoolenbourke Not sure if you're still interested in this issue but I've recently ported await.hpp and impl/await.hpp and thought I would share my findings here since I can't find a discussion on this issue anywhere else.

You mentioned doing a frankenbuild but getting stuck on an issue with ASIO_NO_DEPRECATED and async_result, so I'm going to assume you got the obvious stuff out of the way.

The structure asio::async_result is a type trait that is specialized for completion tokens which are not literal callbacks: it tells ASIO what will be used as the handler and what should be the return value of the async operation. If you're interested in the philosophy and the details you can see http://wg21.link/n3747 .

That paper however describes the deprecated ASIO functionality, but the adjustment is fairly straightforward. In old versions of ASIO (and in the implementation in impl/await.hpp) you had to specialize two traits:

  1. async_result<Handler>, and
  2. handler_type<Signature>,

both of which were required to have a publicly visible alias or typedef called type.

You'll note here that the handler_type trait is deprecated. In the new approach we use async_result<CompletionToken, Signature> which unifies the separate traits above.

The changes are as follows:

-async_result<Handler>::type should now be async_result<CompletionToken, Signature>::return_type, and -handler_type<Signature>::type should now be async_result<CompletionToken, Signature>::completion_handler_type.

It is straightforward to adapt the old, two-trait implementation to this form, carrying over the methods/data of the old async_result.

If you do this you won't be out of the woods just yet, but the other change is minor. The implementation of spawn constructs an asio::async_completion object and then attempts to access its handler member; in newer releases this is now called completion_handler.

With these changes you should be good to go, although I had a minor issue with passing an io_context to the enable_if overloads of spawn. I haven't bothered to look into this in detail or try to fix it; you can work around this by passing io_ctx.get_executor() for your asio::io_context io_ctx rather than passing the io_ctx itself.

akoolenbourke commented 6 years ago

Thanks @cstratopoulos. Since my post I have actually gone and implemented my own "await context" for use with TS coroutines and it's working nicely. All the things you mentioned I ended up discovering and having to deal with also but thanks for taking the time to post.

One thing I never spent the time to look into was the use of the for(;;) co_await code (IIRC) in the spawn process. I wasn't quite sure why that was there but never spent the time to work it out before I just wrote my own. I was hoping I could get the # of spawned coroutines down to lower than the ASIO await code to limit heap allocations.

harenbrs commented 9 months ago

I was hoping that someone would have an answer to the original thread question (polymorphic handling of TCP and SSL streams). What is the recommended approach here?