An RxJS to Rx.NET bridge
tl;dr: JavaScript can call C# code in Metro apps, and allows for C# code to observe notifications coming from JavaScript, using the Reactive Extensions grammar.
Javascript is (almost) everywhere, literally.
Not in everyone's minds though, which is only around 7% according to the PYPL index.
Yet, Microsoft is pushing a lot on that front, particularly in Metro apps where the documentation shows JavaScript examples first. Subjectively, I’m not a big fan of the decision, because it pushes us back a few years and not simply because of the language, but because state of the full JavaScript ecosystem, compared to the .NET counterpart.
[more]
JavaScript ?
Quirkiness of the language aside, things are going in a good direction -- particularly for large applications -- with TypeScript, where type safety is introduced, among other things.
Arguably, JavaScript is not very good a parallelism (which is very different from asynchrony), mainly because it is single-threaded. There are ways around it, like using Web Workers, but this is bare-metal isolated parallelism, where everything needs to be done by hand. Maybe soon enough, we’ll end up with someone reinventing a .NET Remoting (or DCOM) look alike to address these issues.
We’re going in a multi-core CPU direction, because processors scaling up seems to have slowed down quite a bit, and that scaling out is now the trend. The immutability trend seems to rise here and there to embrace this change, C# hints at taking that route with BCL’s immutable collections, and F# has it at its core.
I foresee JavaScript lagging behind in that regard, but maybe that’s not a problem for now because it is used as an HTML back-end (and used it for this only for the time being). We still need to address a lot of concerns with raw performance, particularly when applications are not just displaying data, but are actually performing a lot of business computation on the client side.
Mixing C# and JavaScript
Fortunately, there is a very good way to do actual parallelism in Metro apps because of two things :
- JavaScript can call WinMD files code
- C# can expose code as WinMD files
and in C#, there is Rx.NET that excels at mixing both asynchrony and parallelism. It is very easy to share and use immutable data using Rx and its Query based programming which makes it a good fit.
It’s actually pretty easy to create a mixed environments application :
- Create a WinJS app
- Create a C# Windows Runtime Component, which creates a WinMD file
- Add a reference to the WinMD project in the WinJS project
- Call some code, and voila !
Sounds too good to be that easy ? Yeah, you’re actually right…
Bridging RxJS and Rx.NET
Microsoft’s preferred way for exchanging messaging is through events. Even in WinMD type definitions, and pretty much all APIs in WinRT expose some sort of .NET like events.
I’m not a big fan of events, for subscription lifetime, memory management, composition, delegation, … I’m transforming them into Rx observable where ever I can, and being able to have a common way for exchanging asynchronous notifications, using multi-valued returning functions, between both worlds seems a pretty good way to go.
Both RxJS and Rx.NET seem to share the same notification grammar, meaning that it is should be very easy to map between the two.
But there is a problem with all this, being that JavaScript objects, as far as I know, cannot be marshaled through to .NET. Only WinMD exposed types, primitives and delegates can. If you try to pass a Javascript object, you’ll be greeted with a Type Mismatch exception…
Exposing an RxJS observable to Rx.NET
The idea is to be able to expose an RxJS observable to an Rx.NET C# observer, and subscribe to it.
Note: Excuse my JavaScript, I’m probably using it somehow the wrong way… Feel free to correct me !
First, here is the JavaScript observable we want to subscribe to :
var observable = Rx.Observable.return(42);
To be able to pass this around we need to expose a way to subscribe to that observable inside JavaScript, but allow for C# to provide onNext, onError and onCompleted handlers, but also a way to dispose the created subscription :
Rx.Observable.prototype.toWrapper = function () { var parent = this; return new Umbrella.ObservableWrapper( function (onNext, onError, onCompleted) { var subscription = parent.subscribe( onNext, function (e) { return onError(e.toString()); }, onCompleted ); return function () { subscription.dispose(); } } ); };
For the same reasons we can’t pass the observable directly to C#, we can’t pass the subscription returned by to subscribe back to C#. We also can’t pass the exception as-is.
The trick here is to provide a first function that create the subscription, and return another that disposes it.
The ObservableWrapper class is a WinMD exposed C# class that takes this form :
public sealed class ObservableWrapper { private SubscribeHandler _subscribe;
public ObservableWrapper(SubscribeHandler subscribe) { _subscribe = subscribe; }
internal IObservable<object> AsObservable() { return Observable.Create<object>(o => { var disposeAction = _subscribe( v => o.OnNext(v), e => o.OnError(new Exception(e.ToString())), o.OnCompleted ); return Disposable.Create(() => disposeAction()); }); } }
The wrapper is created with a delegate that calls subscribe, and the observer is decomposed as three lambdas for the notifications. The subscribe delegate create a delegate that will dispose the subscription on the JavaScript side, when the C# observable will be disposed.
Also note that the class is sealed, and that the AsObservable method is internal, because it exposes .NET only types and will only be used from .NET code.
Next, C# methods can then take ObservableWrapper instances as parameters, which can be subscribed to :
public void DoStuff(ObservableWrapper wrapper) { var s = wrapper .AsObservable() .Subscribe( v => Debug.WriteLine(v), e => Debug.WriteLine(e), () => Debug.WriteLine("Completed") ); }
It can then used like this, on the JavaScript side :
var foo = new MyApp.Foo(); foo.doStuff(observable.toWrapper());
All this works pretty well !
I’ll probably talk on how to do it the other way around in another blog post, as there are some other tricks to use to make this work.