4

When compiling managed C++ code with the /clr flag, the compiler does not allow the include. I am trying to port my unmanaged C++ code into a managed C++ environment. I see that C# has the alternatives Task and TaskCompletionSource to replace futures and promises but I do not see these options available in managed C++. I need to perform interop with some C++ unmanaged libraries so I cannot switch to C# completely. I still need a C++ layer in between. How can I achieve future/promise functionality in managed C++?

Here is an example of unmanaged code in C++ which compiles without the /clr flag:

int Foo(std::future<int> &fur) {
    int result = 1;
    int value = fut.get();
    // Do something with value

    return result;
}   

int main() {
    int x;
    std::promise<int> p;
    std::future<int> f = p.get_future();
    std::future<int> fut = std::async(Foo, std::ref(f));
    int val = 1;
    p.set_value(val);
    x = fut.get();
}    

I want to be able to do this in C++/CLI

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
shaveenk
  • 1,983
  • 7
  • 25
  • 37
  • There is always callbacks and state machines to do async. – Daniel Feryance Jul 20 '17 at 21:26
  • I don't get your question. With c++-cli it is clearly possible to mix native and managed. We can use "includes" and "using namespaces". – minus one Jul 21 '17 at 21:08
  • @SchLx When you try to compile managed or unmanaged code with the /clr flag, you are not allowed to use the include. How is it still possible to use it then? – shaveenk Jul 21 '17 at 21:11
  • Ok I see: we get: the error message " is not supported when compiling with /clr or /clr:pure." – minus one Jul 21 '17 at 21:27
  • @SchLx I've looked at the documentation before I created this post. There is a solution by using a ```task``` object in ppltasks.h. This is similar to what a ```Task``` does in C#. But I don't see an equivalent for ```TaskCompletionSource``` which is what a promise is. – shaveenk Jul 21 '17 at 23:19

2 Answers2

3

Update (some of the early comments were reformulated here):

Many of the standard C++ libraries are free to use even if we choose common language runtime support (C++-Cli, CLR). But some of them are not available, for example the questioned ones.

In case we add such header we get the following error:

' <future> is not supported when compiling with /clr or /clr:pure. '

This means for us that such code may stay in a separate dll OR we have to refactor our code OR we need a C# implementation of the mentioned libraries like this one.

Answer:

The question itself contains the correct answer, so the future and primes could be implemented using the following:

  • Task<T> is a future (or Task for a unit-returning future),
  • TaskCompletionSource<T> is a promise,

was also shown in the post here. This means we can simply search C# replacement for future and promise and then translate them to C++-Cli.

In my answer I only show some key points how to translate and use Task and built in delegates. Task itself is easy:

    #include "stdafx.h"

    using namespace System;
    using namespace System::Threading::Tasks;

    public ref class MyActions
    {
    public:
        MyActions()
        {
            // lambdas are not allowed for managed class so we use built in delegates
            auto t = gcnew Task(gcnew Action<Object^>(task1),this);
        }

    public:
        static void task1(Object^ o)
        {
            // TODO:
            printf("Hello World! [c++-cli] and [win32]");
        }
    };

As you can see the C++-Cli syntax is fairly similar to c#, and you still have most of the 'old' C++.

NOTE: We should still keep class definition and implementation separate (myactions.h and myactions.cpp)

minus one
  • 642
  • 1
  • 7
  • 28
  • I don't see you using a promise here. My original question is related to being able to replicate the functionality of a promise. Let me modify my question and add a code block so it is a little clearer. – shaveenk Jul 22 '17 at 01:19
  • @shaveenk: You are right, I did not implement the promise and future for you, instead showed only some key points. The main message is: you can search for [C# equivalent](https://stackoverflow.com/search?q=promise+future+%5Bc%23%5D) (like [this](https://stackoverflow.com/questions/38996593/promise-equivalent-in-c-sharp) post), and change it to c++-cli like shown in my example. – minus one Jul 22 '17 at 09:36
1

C++ / CLI doesn't allow including any of the standard headers <mutex>, <future>, <thread> & <condition_variable> (BTW in VS2017 you could use <atomic>). It also does not allow the use of the concurrency runtime offered by microsoft.

Since you are using C++ / CLI, I assume portability is not an issue. My suggestion is that you roll your own mutex, condition_variable, future and promise using thin wrappers around the windows API.

A while ago I wrote some code that does just that. it is not as hard as it might sound.

Note:

  • this appended code is not meant to be standard conformant in any way.
  • srwlock is a replacement for std::mutex and can even be used with std::lock_guard / std::unique_lock.
  • srwcondition_variable is a replacement for std::condition_variable.
  • unique_srwlock is a replacement for std::unique_lock (for use with srwcondition_variable).
  • srfuture & srpromise are replacements for std::future & std::promise.

:

#include <windows.h>

class srwlock
{
    SRWLOCK _lk{};
public:
    void lock()
    {
        AcquireSRWLockExclusive(&_lk);
    }
    void unlock()
    {
        ReleaseSRWLockExclusive(&_lk);
    }
    void lock_shared()
    {
        AcquireSRWLockShared(&_lk);
    }

    void unlock_shared()
    {
        ReleaseSRWLockShared(&_lk);
    }
    SRWLOCK* native_handle()
    {
        return &_lk;
    }
    srwlock() = default;
    srwlock(const srwlock&) = delete;
    srwlock& operator=(const srwlock&) = delete;
    srwlock(srwlock&&) = delete;
    srwlock& operator=(srwlock&&) = delete;
};


template <typename _Lk>
class unique_srwlock
{
    _Lk& _lk;
    friend class srwcondition_variable;
public:
    void lock()
    {
        _lk.lock();
    }
    void unlock()
    {
        _lk.unlock();
    }

    unique_srwlock(_Lk& lk) : _lk(lk)
    {
        _lk.lock();
    };

    ~unique_srwlock()
    {
        _lk.unlock();
    };

    unique_srwlock(const unique_srwlock&) = delete;
    unique_srwlock& operator=(const unique_srwlock&) = delete;
    unique_srwlock(unique_srwlock&&) = delete;
    unique_srwlock& operator=(unique_srwlock&&) = delete;

};

    enum class srcv_status
{
    timeout, no_timeout
};

class srwcondition_variable
{
    CONDITION_VARIABLE _cv{}; 
public:
    void wait(srwlock& lk)
    {
        VERIFY_TRUE(SleepConditionVariableSRW(&_cv, lk.native_handle(), INFINITE, 0));
    }
    void wait(unique_srwlock<srwlock>& lk)
    {
        VERIFY_TRUE(SleepConditionVariableSRW(&_cv, lk._lk.native_handle(), INFINITE, 0));
    }

    srcv_status wait_for(unique_srwlock<srwlock>& lk, const std::chrono::milliseconds& timeout_duration)
    {
        auto val = SleepConditionVariableSRW(&_cv, lk._lk.native_handle(), static_cast<DWORD>(timeout_duration.count()), 0);

        if (val != 0)
        {
            return srcv_status::no_timeout;
        }
        else
        {
            if (GetLastError() == ERROR_TIMEOUT)
            {
                return srcv_status::timeout;
            }
            else
            {
                throw std::runtime_error("wait_for unexpected return value in SleepConditionVariableSRW");
            }
        }
    }

    void notify_one()
    {
        WakeConditionVariable(&_cv);
    }
    void notify_all()
    {
        WakeAllConditionVariable(&_cv);
    }

    srwcondition_variable() = default;
    srwcondition_variable(const srwcondition_variable&) = delete;
    srwcondition_variable& operator=(const srwcondition_variable&) = delete;
    srwcondition_variable(srwcondition_variable&&) = delete;
    srwcondition_variable& operator=(srwcondition_variable&&) = delete;
};



class bad_srfuture : public std::runtime_error
{
public:
    bad_srfuture(const char* msg) : std::runtime_error(msg) {}
};

inline void throw_bad_srfuture(bool isValid)
{
    if (!isValid)
    {
        throw bad_srfuture("no state");
    }
}

#ifdef _DEBUG
#ifndef FUTURE_THROW_ON_FALSE
#define FUTURE_THROW_ON_FALSE(stmt) throw_bad_srfuture(stmt);
#endif
#else
#ifndef FUTURE_THROW_ON_FALSE
#define FUTURE_THROW_ON_FALSE(stmt) __noop
#endif
#endif


enum class srfuture_status
{
    deffered, ready, timeout
};


namespace private_details
{
    template <typename T>
    class future_shared_state
    {
    public:
        void wait() const
        {
            // wait until there is either state or error
            unique_srwlock<srwlock> lk(_cs);
            while (!_state && !_error)
            {
                _available.wait(lk);
            }
        }
        srfuture_status wait_for(const std::chrono::milliseconds& timeout_duration)
        {
            // wait until there is either state or error
            unique_srwlock<srwlock> lk(_cs);
            while (!_state && !_error)
            {
                auto cv_status = _available.wait_for(lk, timeout_duration);
                if (cv_status == srcv_status::timeout)
                {
                    return srfuture_status::timeout;
                }
            }
            return srfuture_status::ready;
        }
        T& get()
        {
            if (_state) return *_state;
            if (_error) std::rethrow_exception(_error);
            throw std::logic_error("no state nor error after wait");
        }
        template <typename U>
        void set_value(U&& value)
        {
            unique_srwlock<srwlock> lk(_cs);
            if (_state.has_value() || _error != nullptr)
            {
                throw bad_srfuture("shared state already set");
            }

            _state.emplace(std::forward<U>(value));
            _available.notify_all();
        }

        void set_exception(std::exception_ptr e) 
        {
            unique_srwlock<srwlock> lk(_cs);
            if (_state.has_value() || _error != nullptr)
            {
                throw bad_srfuture("shared state already set");
            }
            _error = e;
            _available.notify_all();
        }
    private:
        mutable srwlock _cs; // _state protection
        mutable srwcondition_variable _available;
        std::optional<T> _state;
        std::exception_ptr _error;
    };
} // namespace private_details
template <typename T> class srpromise;

template <typename T>
class srfuture {
public:
    srfuture() noexcept = default;
    ~srfuture() = default;
    srfuture(srfuture const& other) = delete;
    srfuture& operator=(srfuture const& other) = delete;
    srfuture& operator=(srfuture&& other) noexcept = default;
    srfuture(srfuture&&) noexcept = default;

    T get() 
    {
        // get is assumed to be called from a single thread.

        // step 1: pass the _shared_state to the current thread waiter (invalidate)
        // (other threads calling get() will result in undefined behaviour as _shared_state will be nullptr
        auto shared_state = std::move(_shared_state);

        FUTURE_THROW_ON_FALSE(shared_state != nullptr);

        // step 2:  safely wait for the shared state to fulfill
        shared_state->wait();

        // shared state is fulfilled and no exception is set.
        // step 3: move / copy the state:
        return std::move(shared_state->get()); // https://stackoverflow.com/questions/14856344/when-should-stdmove-be-used-on-a-function-return-value   
    }

    bool valid() const noexcept 
    { 
        return _shared_state != nullptr; 
    }

    void wait() const 
    {
        // The behavior is undefined if valid() == false before the call to this function.
        FUTURE_THROW_ON_FALSE(valid());
        _shared_state->wait();
    }

    srfuture_status wait_for(const std::chrono::milliseconds& timeout_duration) const
    {
        FUTURE_THROW_ON_FALSE(valid());
        _shared_state->wait_for(timeout_duration);
    }
private:
    std::shared_ptr<private_details::future_shared_state<T>> _shared_state = nullptr;

    friend class srpromise<T>;
    srfuture(const std::shared_ptr<private_details::future_shared_state<T>>& shared_state) : _shared_state(shared_state) {}
    srfuture(std::shared_ptr<private_details::future_shared_state<T>>&& shared_state) : _shared_state(std::move(shared_state)) {}
};

template <typename T>
class srpromise 
{
public:
    srpromise() : _shared_state(std::make_shared<private_details::future_shared_state<T>>()) {}
    srpromise(srpromise&& other) noexcept = default;
    srpromise(const srpromise& other) = delete;
    srpromise& operator=(srpromise&& other) noexcept = default;
    srpromise& operator=(srpromise const& rhs) = delete;
    ~srpromise() = default;

    void swap(srpromise& other) noexcept
    { 
        _shared_state.swap(other._shared_state);
    }

    srfuture<T> get_future() 
    {
        return { _shared_state };
    }

    void set_value(const T& value)
    {
        _shared_state->set_value(value);
    }

    void set_value(T&& value) 
    {
        _shared_state->set_value(std::move(value));         
    }

    void set_exception(std::exception_ptr p) 
    {
        _shared_state->set_exception(std::move(p));
    }
private:
    std::shared_ptr<private_details::future_shared_state<T>> _shared_state = nullptr;
};
Elad Maimoni
  • 3,703
  • 3
  • 20
  • 37