Abstract
This post shows how to pass data from C# code to a portable, pure C++ layer via the Windows Runtime. This can be interesting to people who write portable C++ libraries, especially for mobile platforms.
The sample solution can be found here: https://github.com/gergo89/C-CppInteropabilityViaWinRtBlogDemo
Background story (skip this if you are only interested in the technical stuff)
To understand the whole story here is a short history about the post: As my Master's Thesis I implement a database synchronisation framework which can synchronise a SQLite database with MSSql (and possible with other DBMSs later…) including change tracking, conflict resolution, etc… The core of the framework is written in C++ with STL and it can be compiled for all the major mobile platforms (IOS, Android, Windows Runtime (aka. Windows Store Apps+Windows Phone)). To integrate such a framework with the native dev. environment you usually write some kind of wrapper classes. On IOS you use Objetive-C++, for Android you use NDK and JNI and for Windows you build Windows Runtime components to expose the C++ classes to the higher level languages (like C# and JS).
While I was working on my Windows Runtime wrapper I had the following problem: I have a cool C# library that worked great on Windows Runtime and I wanted to pass the data which was created by this C# library to my pure C++ layer. This C++ layer is shared between IOS, Android and Windows, so at this layer no platform specific stuff is allowed. The post shows how I solved the problem! I also attached a sample solution which is a very basic example. In the explanation I concentrate on the interoperability problem and I don't talk about the specifics of my sync framework in order to keep the explanation as simple as possible. In the explanation I focus on the attached sample and only give some insides to my concrete scenario (again: to keep the post simple…)
Architecture
The first figure shows the general architecture on Windows. At the bottom the shared C++ layer is the one where the entire portable C++ code lives and this layer is cross compiled across different platforms.
And what we want to have is that the App specific C# code calls to the shared C++ library which then requests data from the C# library (right box in the middle) via a Windows Runtime component. As soon as the shared C++ library has the data it can process it and then it can push the results to the C# code.
Btw. it is very common in such architecture that the execution makes this "U" across the layers. For example the mobile platforms have great networking stuff which you don't want to reemployment in C++ (ok, some people want, but in most cases it just doesn’t make any sense). In this case you abstract the network communication away and connect it for every platform via an interface, which causes the same architecture.
Now let's talk about the sample code
In the sample solution there is a C++ "framework" which requests a std?vector<int> from an interface, it increments the items in the vector and returns it. The data source in this case is a C# class. So what happens is that the Store App triggers a request to a Windows Runtime component, it goes into the C++ layer, then it goes back to the Windows Runtime component, then it goes to C# to get the data, it returns it to the C++ layer, does the computation and passes it back to C#. (Sounds silly, but there are so many scenarios where this makes sense as long as you don't overuse it)
The solution contains three projects: (the project names are on the right side of the first two figures)
-
· SharedCppLayer: This simulates the C++ layer. There is no platform specific code here and it compiles on every major platform. In my original scenario (Db sync framework) this is the code where the core of my framework lives: change tracking, database initialization, conflict resolution, etc. In the sample code this "framework" requests a std::vector<int> via an interface, it increments every item and returns it as a response. (again: I concentrate on the architecture and minimize the code to keep this simple)
-
· WindowsRuntimeComponent: This is the Windows Runtime Component which exposes the framework to higher level languages like C# and JS. This is normally a very thin layer, since you implement this on every platform, so we want to keep it as thin as possible. This project uses C++/CX so it can reference the SharedCppLayer and all the public ref classes are exposed to the StoreApp layer.
-
· StoreApp: This is the user code. It is the client of the framework.
How does this work
Now let's see the code! We go from bottom to top (C++ → C++/Cx → C#)
The first layer contains two classes:
CppLogic: This would be the class which contains all the C++ logic which is exposed to C#. It has one method: std::vector<int> IncrementItems() which requests data from a data source and increments every item in the vector and returns it (again: this is in a real life example much more complex code). It also has a std::unique_ptr to an instance of the IDataSource which serves as the data source to the library. The implementation of this interface is platform dependent. One other solution to solve this would be to use ifdefs in the C++ code and check for the platforms every time the CppLogic requests data, but believe me this is not what you want!
class CppLogic
{
std::unique_ptr dataSource;
public:
CppLogic(std::unique_ptr dataSource_):
dataSource(std::move(dataSource_)){};
std::vector IncrementItems();
};
IDataSource: It defines the interface to receive data
class IDataSource
{
public:
virtual std::vector GetData() = 0;
};
So this was the portable C++ library. Normally this is a lot of portable C++ code which is compiled for every platform. Here we keep it simple. The WindowsRuntimeComponent layer is where the show really begins: (keep in mind that we have C++/CX here, so if you don't know what that is and you see strange syntax, then google it.) The CppLogicWrapper class exposes the CppLogic C++ class to C# (that is the public ref before the class keyword). It has also an IncrementItems function which calls to the original IncrementItems from C++.
public ref class CppLogicWrapper sealed
{
public:
CppLogicWrapper();
Windows::Foundation::Collections::IVector^
IncrementItems();
};
The DataSource class is the platform dependent implementation of the IDataSource interface and it is passed to the CppLogic instance which receives data from it.
class DataSource: public IDataSource
{
public:
DataSource();
~DataSource();
std::vector GetData(){
return WindowsRuntimeComponent::WinRtDataSource::GetData();
}
};
As you can see the GetData function calls the GetData function of the WinRtDataSource class, which is a C++/Cx class. How can this work? A pure C++ calls a C++/Cx function!?: well it works, because the GetData function is an internal function (not exposed, so it is a valid, normal C++ function). So this is where C++ and C++/Cx connects. Now let's move on to the WinRtDataSource class where C++/Cx calles into C#
public delegate Windows::Foundation::Collections::IVector^
DataProviderDelegatet(void);
public ref class WinRtDataSource sealed
{
private:
WinRtDataSource();
DataProviderDelegatet^ getList_;
static WinRtDataSource^ staticInstance_;
internal:
static std::vector GetData();
public:
property DataProviderDelegatet^ ListDelegate
{
DataProviderDelegatet^ get()
{ return getList_; }
void set(DataProviderDelegatet^ data)
{ getList_ = data; }
}
static property WinRtDataSource^ statInstance{
WinRtDataSource^ get(){
if (WinRtDataSource::staticInstance_
== nullptr){
staticInstance_ = ref new WinRtDataSource();
}
return staticInstance_;
}
}
};
The first thing that is defined in the header file of the WinRtDataSource class is a delegate which can point to a method which returns an IVector and has no parameters. The Class has one instance of this delegate which can be inserted from outside (everything after the public keyword is visible from C# and JS). So from C# we can point to the C# method which delivers the data. Now we can move on to the implementation of the static std?vector GetData() method which is called via the IDataSource implementation in the CppLogic class.
std::vector WinRtDataSource::GetData(){
std::vector retVal;
if (statInstance->getList_ != nullptr){
auto items = statInstance->getList_->Invoke();
for (auto item : items){
retVal.push_back(item);
}
return retVal;
}
else {
throw ref new Platform::NullReferenceException
("Delegate to the datesource for the CppLogicWrapper is not set");
}
}
So what happens here is that the singleton instance of the WinRtDataSource goes to its getList_ delegate and triggers the function if it's not null and returns an IVector. This data obviously must be converted to std?vector, that is basically what the rest of the function does (plus some error handling) and returns this std?vector to the C++ layer. So this is how a C# List becames a std?vector. After this point the C++ library can do its job, calculate the new values, and return it to the C++/Cx wrapper, which returns that to C #.
In the C# layer I created a class to simulate the C# library:
class CsLibrary
{
public List GetList()
{
return new List { 1, 2, 3, 4, 5 };
}
}
This is the method which provides the data to the C++ layer.
To use the wrapper class the user code in C# looks like this:
var csLib = new CsLibrary();
var cppWrapper = new WindowsRuntimeComponent.CppLogicWrapper();
WindowsRuntimeComponent.WinRtDataSource.statInstance.ListDelegate =
csLib.GetList;
var result = cppWrapper.IncrementItems();
List list = new List(result);
As you can see the user must set the ListDelegate to the C# method which delivers the data. This is one downside of this architecture: normally I would not expose such an important step to the user code, but that's how this works now….
Conclusion
The main point of the post was to show that you can pass data from a platform specific library into a C++ layer which is compiled across different platforms. All this is nicely supported by the Windows Runtime. You can completely avoid ifdefs in the C++ code. Two weak points of the sample implementation are the singleton in the wrapper layer and the fact that the user code has to wire up the delegate in C#. With some refactoring you can get rid of these problems very easily, but I wanted to keep the example as simple as possible.