[Twisted-Python] Deferred in C++

Hi, I've implemented Deferred in C++. There is room for improvement and optimization, but the basic core works. My focus has been on trying to make the best possible API experience for users. The internal implementation is confusing and full of complexity unrelated to the problems being solved. For example, because the implementation relies on templates so heavily, order-of-definition is an issue. Luckily most of the complexity stays hidden away leaving a mostly easy-to-use API, even in tricky cases such as callback pipelines that change the result type being passed from one callback to another by the Deferred: int convert_string(std::string result) { std::cout << "Converting string " << result << std::endl; return ion::Convert::to_integer<int>(result); } int add_ten(int result) { std::cout << "Adding 10 to " << result << " and returning " << result + 10 << std::endl; return result + 10; } Deferred deferred = Deferred::create_deferred<std::string>(); deferred.add_callback(&convert_string); deferred.add_callback(&add_ten); std::cout << "Calling succeed" << std::endl; deferred.succeed(std::string("13")); std::cout << "Callbacks finished" << std::endl; Running this code will produce the following output: Calling succeed Converting string 13 Adding ten to 13 and returning 23 Callbacks finished It's possible to use method pointers as callbacks, too: class CallbackHandler { public: int handle_result(int result) { std::cout << "Handling " << result << std::endl; return result; } }; CallbackHandler handler; deferred.add_callback(&handler, &CallbackHandler::handle_result); One of the constraints that the API mostly hides is that the return and parameter types of every callback in the pipeline need to be known at compile time. add_callbacks, add_callback and add_errback are template methods that provide implementations for different combinations of function and method pointers. They infer return and parameter types, based on the inputs passed in, and use them to create internal structures. Inferring types breaks down when adding a callback that returns a Deferred because there's no way to infer the result type of a Deferred that may not exist yet. In such cases, the result type of the Deferred must be explicitly stated: class DeferredCallback { public: DeferredCallback() : deferred(Deferred::create_deferred<int>) {} Deferred get_deferred(int) { std::cout << "Returning deferred" << std::endl; return this->deferred; } Deferred deferred; }; Deferred deferred = Deferred::create_deferred<std::string>(); DeferredCallback callback; // Notice the explicit result type here: deferred.add_callback<int>(&callback, DeferredCallback::get_deferred); deferred.add_callback(&add_ten); std::cout << "Calling succeed" << std::endl; deferred.succeed(std::string("13")); std::cerr << "Calling deferred" << std::endl; callback.deferred.succeed(6) std::cout << "Callbacks finished" << std::endl; Running this code will produce the following output: Calling succeed Returning deferred Calling deferred Adding ten to 6 and returning 16 Callbacks finished When a callback returns a Deferred, processing of the pipeline pauses until a result comes available, which is why add_ten is only called after the inner Deferred succeeds. If you forget the explicit result type when adding a Deferred returning callback you'll get a compile-time error: ../../ion/defer.h: In member function 'void ion::Deferred::add_callbacks(Instance*, ion::Deferred (Instance::*)(Param), ion::Deferred (Instance::*)(ion::trule<ion::Failure, ion::DeallocObject>)) [with Instance = ion::tests::DeferredMethodHandler, Param = int32_t]': test-defer.cpp:416: instantiated from here ../../ion/defer.h:773: error: no matching function for call to 'ion::CompileTimeAssertion<false>::CompileTimeAssertion(ion::Deferred::add_callbacks(Instance*, ion::Deferred (Instance::*)(Param), ion::Deferred (Instance::*)(ion::trule<ion::Failure, ion::DeallocObject>)) [with Instance = ion::tests::DeferredMethodHandler, Param = int32_t]::STATIC_ASSERT_Deferred_result_type_must_be_specified&)' ../../ion/debug.h:164: note: candidates are: ion::CompileTimeAssertion<false>::CompileTimeAssertion() ../../ion/debug.h:164: note: ion::CompileTimeAssertion<false>::CompileTimeAssertion(const ion::CompileTimeAssertion<false>&) ../../ion/defer.h: At global scope: ../../ion/defer.h:772: warning: unused parameter 'instance' ../../ion/defer.h:772: warning: unused parameter 'method_callback' ../../ion/defer.h:772: warning: unused parameter 'method_errback' If you look *really* hard you'll see some helpful information starting with STATIC_ASSERT. Yes, that's a feature. I've implemented a Failure object, but failure handling over all is not very good. Exceptions are tricky because, unless you're expecting a particular kind of exception, their types become opaque as soon as they're thrown. There's room for improvement but there's no obvious path to a good solution yet. The behaviour of errbacks is slightly different than in the Python implementation. Errbacks receive a trule<Failure> (a scoped pointer that the errback owns) and return a result suitable for the next callback. They can either (a) recover from the error and return a result, (b) raise an exception to indicate a new error or (c) raise an UnhandledFailureError exception with the failure object. Callback processing continues after (a), while errback processing continues after (b) or (c). A new Failure object is created when (b) occurs. int recover(trule<Failure>) { std::cout << "Recovering from failure" << std::endl; return 43; } Deferred deferred = Deferred::create_deferred<int>(); deferred.add_errback(&recover); deferred.add_callback(&add_ten); std::cout << "Calling fail" << std::endl; deferred.fail(Exception("Error message!")); std::cout << "Callbacks finished" << std::endl; Running this code will produce the following output: Calling fail Recovering from failure Adding ten to 43 and returning 53 Callbacks finished I'm not sure how practical this component would be in a non-trivial application. The reliance on templates would negatively affect compile time, for one thing. More importantly, I've found deciphering the error messages produced by simple mistakes can be tricky. Nonetheless, my main motivation was to see if it was possible at all and so I'm pleased that it works in the end. In the future, I'd like to see what the performance difference is between this version and the Python version, but I haven't investigated that at all. There are also memory-management related features that could be added to ease using this in C++ that would be interesting to explore. Porting to Windows is another item on the list of things that I'd like to do. The code is part of a project called ion (I/O and Networking library), which I use for experiments like this. I haven't merged the deferred branch yet, but will soon. Until it's merged, it's on Launchpad: https://code.edge.launchpad.net/~jkakar/ion/deferred It will most likely only build on Ubuntu Intrepid right now. Thanks, J.

Hello, On Sat, Oct 18, 2008 at 22:34, Jamu Kakar <jkakar@kakar.ca> wrote:
Wonderful! Awesome! So I am not the only one who tries to use Twisted concepts in C++. I did write some kind of a Deferred class in C++ myself, together with a re-implementation of a Twisted framework (two reactors featuring TCP client & server, callLater, callInThread...; a selectReactor and a native MFC reactor that merges nicely in a Windows application, yes, there are some; Protocols, LineReceiver, HttpClient & server...) and I already have a running business application that uses all of it on Windows. The Deferred part was really hard to implement, and may be the reason why it was never done before. It really pushed me out of my C++ knowledge. I tried to convert my code to use your class, which seems easier to use than mine. Great job!
I'm not sure how practical this component would be in a non-trivial application.
Here are random comments about the changes I had to make, when confronted to a real application using nested deferreds, DeferredList, various error handling... - First, good news, your code almost compiles on Windows (VS 2005). I had to typedef int32_t (why don't you simply use int?), comment out the StackFrameFactory, and declare some functions/classes with __declspec(dllexport) (with the help of a macro) so we can build a DLL. - In my eyes, the notation "addCallback" is more readable than "add_callback" (and it's the one used by twisted python), but that's a matter of taste. On the other hand I found disturbing at first to fire deferreds with the methods "succeed()" and "fail()" (instead of callback() and errback() - I found that my callbacks often have a void result, and deferred are sometimes fired without a value. For example, a twisted DeferredList often just waits for its children to finish. The results may be gathered somewhere, but the callbacks return None. I took the liberty to change your code and add *a lot* of template specializations for the "void" ReturnType or ResultType. I suggest that when a callback takes no parameter, it can be added to any Deferred, whatever its current state (and the current value is simply discarded). - In some cases my callbacks take additional arguments (the twisted "callbackArgs"). To make it possible I used the boost::bind functions, and a wrapper class that converts any boost::function to a ion::ICallback. - Several times I had a "Can't add invalid callback" error, and I found it difficult to figure which type was in the deferred state (the previous callback returned a Deferred, and so on...) So I extended ITypeInfo with a "const char* typename" member, that I fill with "typeid(Object).name()". Much easier to see with the debugger... - I tried to implement a DeferredList. I almost succeeded, except that it simply inherit from Deferred and it seems a bad idea: when the DeferredList is returned as a plain Deferred, all state is lost... and the application crashes. Maybe Deferred should be a kind of "shared pointer" to the real object: DeferredState or DeferredListState - Errr... I'm not sure I understood the "trule" concept. When do you use it? What are its semantics regarding ownership? is it similar to std::auto_ptr? - How is one supposed to get the content of a Failure? I use code like this, but it seems too long for this simple task: void MyProcess::displayError(ion::trule<ion::Failure> failure) { std::string message = ion::ptr<ion::Failure>(failure)->get_message(); [...] throw ion::UnhandledFailureError(failure); } - likewise, it should be possible to simply throw Failure("Some error message");
I confirm: it is.
Nonetheless, my main motivation was to see if it was possible at all and so I'm pleased that it works in the end.
I think my program can be considered as a non-trivial application (I cannot show its code, though, except for the non-business framework) It is definitely possible to implement a Twisted framework in C++. The hard part is to make it nice for the developer. Programming with callbacks already twists your mind, it becomes very difficult if the language syntax hides the important part of the code. The "ion" project seems promising! What extend do you intend to give it? -- Amaury Forgeot d'Arc

On 21 Oct 2008, at 17:14, Amaury Forgeot d'Arc wrote:
Browsing the squid source code the other day, I noticed that it too uses a deferred like device. Matthew Glubb Technical Partner email: matthew.glubb@madebykite.com phone: 44 (0) 7715 754017 skype: mglubb Kite http://madebykite.com -- GPG: 96FF DE0E 0B7B 37F0 7F8D C54C E285 3D8F 5625 9244

On Tue, Oct 21, 2008 at 18:23, Matthew Glubb wrote:
Do you know where it is? a class or function name? I could only find a "DeferredRead" class, which seems to be a wrapper around an asynchronous socket. Twisted's (and Jamu's!) deferreds are much more versatile and apply to anything. -- Amaury Forgeot d'Arc

Hi Amaury, On Tue, Oct 21, 2008 at 5:14 PM, Amaury Forgeot d'Arc <amauryfa@gmail.com> wrote:
So I am not the only one who tries to use Twisted concepts in C++.
Heh, it's lonely being a C++ nerd, isn't it. ;) I'm mostly interested in seeing what's possible and trying to understand how to port very interesting APIs from Python to C++.
The most difficult part for me was dealing with Deferred-returning callbacks and getting the pausing/unpausing behaviour to work correctly. The difficult part was that all the template functions caused order of definition issues. The DeferredTypeInfo was introduced for this case to make Deferred a template parameter and, therefore, work around the ordering issues. It's really heinously complicated inside and I have to decipher it each time I look at it. It's always a bad sign when you're the author of the code you can't comprehend. :)
Cool, glad to hear it. I didn't expect problems, but just haven't had time to port it (and also have VS2005 as my base target compiler for Windows). I plan to s/int32_t/int/, but haven't gotten around to it. It's a historic artifact in that code from many years ago that I've continued for the time being to keep things consistent.
The style issue is entirely personal, so yeah, I can see why you feel that way. As far as succeed() and fail() vs. callback() and errback(), this change was a suggestion from Christopher Armstrong. Apparently, a lot of Twisted users have been confused by callback (the callable you add to a Deferred) vs callback (the method you call to fire the Deferred), and similar for errback.
This is on my list of things to add, but haven't gotten around to it yet. My plan is to throw a NoResultError or something whenever a callback is added to a Deferred that already has void-returning callback at the end of the chain. Though, hmm... I guess you might still want to add an errback to handle errors from the void-returning callback.
Ah, nice. Yeah, I was wondering about that and have been considering implementing a specialized ICallback that can store arguments.
Good call. Yeah, I've had the same issue interpreting InvalidCallbackError's.
Deferred is a "shared pointer" to DeferredState...? Maybe I'm not understanding your point?
This is an implementation of the Holder/Trule pattern described in Josuttis' "C++ Templates" book. There is a reason, which I can't recall right now, that makes returning a ptr (or auto_ptr) from a function potentially dangerous (and also passing one in) that trule fixes. My plan is to migrate to the Boost pointers that will become part of the standard library in C++0x. This will make working with smart pointers in ion much nicer. The ptr/trule pattern is quite cumbersome, especially when templates are involved.
The displayError above is what you're expected to do, so far. If there's a better way I'd love to hear it. There's actually a deeper issue that makes Failure/error handling in this implementation kind of crappy. The type of an exception is lost when caught in a catch block for a base class of the thrown exception. Which means that it's not possible, currently, to determine the concrete exception type from a Failure. One thing I've been considering is making the ion::Exception class provide some extra functionality to make this possible, but I'm unhappy that doing that will make implementing custom ion::Exception's harder. Being able to throw a Failure with a string message is a good idea.
Heh. :)
This "make it nice for the developer" is the primary goal I have with this effort.
The "ion" project seems promising! What extend do you intend to give it?
I'm interested in getting to the point where there's a Twisted-style reactor and, eventually, support for using AMP-based protocols. Thanks, J.

Hello, On Sat, Oct 18, 2008 at 22:34, Jamu Kakar <jkakar@kakar.ca> wrote:
Wonderful! Awesome! So I am not the only one who tries to use Twisted concepts in C++. I did write some kind of a Deferred class in C++ myself, together with a re-implementation of a Twisted framework (two reactors featuring TCP client & server, callLater, callInThread...; a selectReactor and a native MFC reactor that merges nicely in a Windows application, yes, there are some; Protocols, LineReceiver, HttpClient & server...) and I already have a running business application that uses all of it on Windows. The Deferred part was really hard to implement, and may be the reason why it was never done before. It really pushed me out of my C++ knowledge. I tried to convert my code to use your class, which seems easier to use than mine. Great job!
I'm not sure how practical this component would be in a non-trivial application.
Here are random comments about the changes I had to make, when confronted to a real application using nested deferreds, DeferredList, various error handling... - First, good news, your code almost compiles on Windows (VS 2005). I had to typedef int32_t (why don't you simply use int?), comment out the StackFrameFactory, and declare some functions/classes with __declspec(dllexport) (with the help of a macro) so we can build a DLL. - In my eyes, the notation "addCallback" is more readable than "add_callback" (and it's the one used by twisted python), but that's a matter of taste. On the other hand I found disturbing at first to fire deferreds with the methods "succeed()" and "fail()" (instead of callback() and errback() - I found that my callbacks often have a void result, and deferred are sometimes fired without a value. For example, a twisted DeferredList often just waits for its children to finish. The results may be gathered somewhere, but the callbacks return None. I took the liberty to change your code and add *a lot* of template specializations for the "void" ReturnType or ResultType. I suggest that when a callback takes no parameter, it can be added to any Deferred, whatever its current state (and the current value is simply discarded). - In some cases my callbacks take additional arguments (the twisted "callbackArgs"). To make it possible I used the boost::bind functions, and a wrapper class that converts any boost::function to a ion::ICallback. - Several times I had a "Can't add invalid callback" error, and I found it difficult to figure which type was in the deferred state (the previous callback returned a Deferred, and so on...) So I extended ITypeInfo with a "const char* typename" member, that I fill with "typeid(Object).name()". Much easier to see with the debugger... - I tried to implement a DeferredList. I almost succeeded, except that it simply inherit from Deferred and it seems a bad idea: when the DeferredList is returned as a plain Deferred, all state is lost... and the application crashes. Maybe Deferred should be a kind of "shared pointer" to the real object: DeferredState or DeferredListState - Errr... I'm not sure I understood the "trule" concept. When do you use it? What are its semantics regarding ownership? is it similar to std::auto_ptr? - How is one supposed to get the content of a Failure? I use code like this, but it seems too long for this simple task: void MyProcess::displayError(ion::trule<ion::Failure> failure) { std::string message = ion::ptr<ion::Failure>(failure)->get_message(); [...] throw ion::UnhandledFailureError(failure); } - likewise, it should be possible to simply throw Failure("Some error message");
I confirm: it is.
Nonetheless, my main motivation was to see if it was possible at all and so I'm pleased that it works in the end.
I think my program can be considered as a non-trivial application (I cannot show its code, though, except for the non-business framework) It is definitely possible to implement a Twisted framework in C++. The hard part is to make it nice for the developer. Programming with callbacks already twists your mind, it becomes very difficult if the language syntax hides the important part of the code. The "ion" project seems promising! What extend do you intend to give it? -- Amaury Forgeot d'Arc

On 21 Oct 2008, at 17:14, Amaury Forgeot d'Arc wrote:
Browsing the squid source code the other day, I noticed that it too uses a deferred like device. Matthew Glubb Technical Partner email: matthew.glubb@madebykite.com phone: 44 (0) 7715 754017 skype: mglubb Kite http://madebykite.com -- GPG: 96FF DE0E 0B7B 37F0 7F8D C54C E285 3D8F 5625 9244

On Tue, Oct 21, 2008 at 18:23, Matthew Glubb wrote:
Do you know where it is? a class or function name? I could only find a "DeferredRead" class, which seems to be a wrapper around an asynchronous socket. Twisted's (and Jamu's!) deferreds are much more versatile and apply to anything. -- Amaury Forgeot d'Arc

Hi Amaury, On Tue, Oct 21, 2008 at 5:14 PM, Amaury Forgeot d'Arc <amauryfa@gmail.com> wrote:
So I am not the only one who tries to use Twisted concepts in C++.
Heh, it's lonely being a C++ nerd, isn't it. ;) I'm mostly interested in seeing what's possible and trying to understand how to port very interesting APIs from Python to C++.
The most difficult part for me was dealing with Deferred-returning callbacks and getting the pausing/unpausing behaviour to work correctly. The difficult part was that all the template functions caused order of definition issues. The DeferredTypeInfo was introduced for this case to make Deferred a template parameter and, therefore, work around the ordering issues. It's really heinously complicated inside and I have to decipher it each time I look at it. It's always a bad sign when you're the author of the code you can't comprehend. :)
Cool, glad to hear it. I didn't expect problems, but just haven't had time to port it (and also have VS2005 as my base target compiler for Windows). I plan to s/int32_t/int/, but haven't gotten around to it. It's a historic artifact in that code from many years ago that I've continued for the time being to keep things consistent.
The style issue is entirely personal, so yeah, I can see why you feel that way. As far as succeed() and fail() vs. callback() and errback(), this change was a suggestion from Christopher Armstrong. Apparently, a lot of Twisted users have been confused by callback (the callable you add to a Deferred) vs callback (the method you call to fire the Deferred), and similar for errback.
This is on my list of things to add, but haven't gotten around to it yet. My plan is to throw a NoResultError or something whenever a callback is added to a Deferred that already has void-returning callback at the end of the chain. Though, hmm... I guess you might still want to add an errback to handle errors from the void-returning callback.
Ah, nice. Yeah, I was wondering about that and have been considering implementing a specialized ICallback that can store arguments.
Good call. Yeah, I've had the same issue interpreting InvalidCallbackError's.
Deferred is a "shared pointer" to DeferredState...? Maybe I'm not understanding your point?
This is an implementation of the Holder/Trule pattern described in Josuttis' "C++ Templates" book. There is a reason, which I can't recall right now, that makes returning a ptr (or auto_ptr) from a function potentially dangerous (and also passing one in) that trule fixes. My plan is to migrate to the Boost pointers that will become part of the standard library in C++0x. This will make working with smart pointers in ion much nicer. The ptr/trule pattern is quite cumbersome, especially when templates are involved.
The displayError above is what you're expected to do, so far. If there's a better way I'd love to hear it. There's actually a deeper issue that makes Failure/error handling in this implementation kind of crappy. The type of an exception is lost when caught in a catch block for a base class of the thrown exception. Which means that it's not possible, currently, to determine the concrete exception type from a Failure. One thing I've been considering is making the ion::Exception class provide some extra functionality to make this possible, but I'm unhappy that doing that will make implementing custom ion::Exception's harder. Being able to throw a Failure with a string message is a good idea.
Heh. :)
This "make it nice for the developer" is the primary goal I have with this effort.
The "ion" project seems promising! What extend do you intend to give it?
I'm interested in getting to the point where there's a Twisted-style reactor and, eventually, support for using AMP-based protocols. Thanks, J.
participants (3)
-
Amaury Forgeot d'Arc
-
Jamu Kakar
-
Matthew Glubb