Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Mission Double Edge: host node.js in .NET process #17

Closed
tjanczuk opened this Issue · 90 comments

9 participants

@tjanczuk
Owner

This is to support hosting node.js within a .NET process while enabling the same interop model between the two.

@tjanczuk
Owner

In general this requires that node.js supports a hosted model (think node.dll or node.so). An interim workaround for some scenarios may be to provide a thin node.js wrapper app that loads a manged *.exe into node.exe and invokes the standard entry point.

@jeremydmiller

Just letting you know that the FubuMVC would dearly love to have this feature. Somebody has dreams of hosting Mimosa.js in a .Net process or using node.js as a CoffeeScript/Less/etc. compiler from .Net too.

Dunno how much help we could swing your way, but I'd be happy to try to drum up some.

@tjanczuk
Owner

Thanks! Let me consider the best approach to this and how I can use help.

@dobbym

+1

@jeremydmiller
@glennblock
Collaborator
@jeremydmiller
@glennblock
Collaborator
@glennblock
Collaborator
@glennblock
Collaborator
@jeremydmiller
@glennblock
Collaborator
@tjanczuk
Owner

I am about to spike something here. If anyone is interested in collaborating, raise your hand.

@tjanczuk
Owner

@glennblock @filipw @gaulinsoft All input and collaboration welcome.

In my prototype I can now script Node.js code from a .NET application using the very same Edge.js inerop model that exists today.

I did some initial investigations on Windows and I think all the technical areas I perceived risky are now prototyped and working. Here is the gist.

Node needs to be compiled to a shared library rather than executable. Edge needs to be re-compiled against that library (rather than the link library associated with node executable). We will also need another managed CLR library that bootstraps the V8 and Node inside of a CLR process and provides the .NET APIs that .NET application can call to create CLR proxies to Node functions running in-process. It is that library that will need to be referenced by a CLR application to use Edge. This is how the API could look like:

using System;
using Edge.Js;

class Program
{
    async void SomeMethod()
    {
        var func = Edge.Func(@"
            function (data, callback) {
                callback(null, 'Node.js welcomes ' + data);
            }
        ");

        // func is Function<object,Task<object>>

        Console.WriteLine(await func(".NET")); // prints "Node.js welcomes .NET"
    }
}

This is the scope of work needed for this to happen:

  1. Building. We need mechanisms for Windows, Mac, and Linux to build Node shared library and compile Edge.js against that library instead of Node executable.
  2. API. We need a managed library for CLR and Mono that exposes the API above to the application and is also responsible for bootstrapping Node/V8 engine inside of a CLR/Mono process.
  3. Packaging. We need to ship the bits for Windows, Mac, and Linux. For Windows I think we should ship pre-compiled binaries through NuGet (in that process we will need to make an opinionated choice of which version of Node.js to use). For Mac we can have a brew formula that does the necessary building. For Linux we can have a set of instructions and/or a script, similar to what was done for building Mono on Linux. One dimension of this is whether we need to support both x86 and x64, or just x64.
  4. Tests. We need a comprehensive set of tests. A reasonable start is to review and replicate some of the tests that exist currently, only in the reverse direction (.NET calling Node).
  5. Samples. We need interesting samples, building up from simple use cases like the one above, to hosting Node in ASP.NET, to using WebSockets through Node, etc.
  6. Documentation. The readme.md need a slew of new collateral to explain the concepts, give examples, etc.

If you feel like helping with any of the above (writing code, commenting, or sending in beer) please let me know.

@ztone

:+1: Awesome Tomasz! This is really sweet stuff and needs to be done. Love the name btw, "double edge" :)

@glennblock
Collaborator

Very exciting Tomek this looks slick!!!!!!!! :+1:

Adding @tjfontaine, and @jeffhandley who I think both may be interested. Jeff from the nuget side, TJ from node.

The API looks clean and has good parity. I am assuming Func will allow you to allow load JS files directly in a similar fashion to how you can load CSX files?

One idea would be to use npm as the main delivery mechanism as it has good support for building C++ solutions (via node-gyp). There's so many different distros of Linux that you need to have a solution that supports build.

On the nuget side first inclination would be you could have a package that includes a powershell script, but that would be problematic as it doesn't support *nix. So another idea would be to have a nuget package that contains a binary which shells out to NPM or which even talks directly to the npm registry (I wonder if someone wrote this) and pulls down the package.

I think having a requirement that NPM is present is not unreasonable.

@tjanczuk
Owner

The API will have a few modes of passing JS code in. Above is just the moral equivalent of specifying a C# async lambda expression in Node.js. I imagine we will also allow specifying a *.js file. It is interesting to consider whether the *.js file should just follow the conventions of a Node.js module (i.e. export variable etc), but then I really only expect to get one function out of it of the shape function (data, function(error, result)), so there would have to be some convention on top of the module semantics. Not sure it is worth it.

I am not so sure about choosing NPM as the main delivery mechanism. In fact we don't even need to require Node.js to be installed to script Node.js from C#: the shared Node.js library will be bundled in. The built-in Node.js modules offer a reasonable mileage, so in some scenarios folks won't even need to install node. In cases where they do need extra modules, they need to install Node&NPM spearately, and then npm install the dependencies.

Given that I am more inclined to use NuGet as the self-contained distribution mechanism, at least on Windows.

Mac and Linux story will require building. But given that we need to build Node.js shared library, which takes minutes, I am not sure npm install is the right time to do it. I think folks have lower latency expectations from npm install experience. That is one reason we are building Mono as a brew formula rather than doing from within node-gyp.

@dpen2000

I'll help out if I can. Probably with Tests and Samples to start with, though I'd try to dig deeper to get these working too.

@tjanczuk
Owner

So I have that working now: https://twitter.com/tjanczuk/status/464117898630295552. What remains to be done is adding test, samples, and updating documentation.

@dpen2000 The documentation of the project (the main README.md) is already a bit overwhelming, and is only going to get more so with the new mode (scripting Node from C#). I was thinking of re-structuring it along these axis:

  1. Common topics (general interop model, goals).
  2. Installation/prerequisites/building instructions for the 6 areas as a cross product of ("Scripting C# from Node.js", "Scripting Node.js from C#") x (Windows, Mono, MacOS)
  3. A set of how-to guides specific to a task (marshaling data, exception handling, etc).

@dpen2000 let me know if you are willing to spend some time thinking and reorganizing the documentation. I will appreciate it a lot.

BTW the double edge work happens in the "doubleedge" branch.

@glennblock
Collaborator
@Alxandr

Wow. Talk about timing. I was just out looking for something like this, and it was "published" 7 hrs ago! Any chance of there being pre-release nuget packages available somewhere? Automatically built or not.

@tjanczuk
Owner

@glennblock You are the man!

@Alxandr I will put some basic instructions in the readme in the doubleedge branch and publish the nuget package so that we can have a few more people playing with it (maybe @glennblock or @filipwoj want to try something with scriptcs as well). But let's hold off going broad with this until it lands in the main branch with tests & al. I will try to get to this some time today.

@Alxandr

I would recommend creating a myget stream for "nightlies" (or in this case "bleeding edge" xD) to publish completely untested stuff as nuget packages to allow people to test things out (at their own risk). Btw, I tried compiling the double edge branch, and there were much crashing...

@tjanczuk
Owner

Good idea with MyGet. Here is the feed you can get Edge.js package from (pssst, this is a secret URL, please don't share on just yet): https://www.myget.org/F/edge/auth/3117df9a-a215-43b3-91f8-fb0a4b9f4cc0/

In terms of building, what issues did you run into? The way to build double edge is to run https://github.com/tjanczuk/edge/blob/doubleedge/tools/build_double.bat. It will take several minutes, downloading node.js sources, compiling them for 2 flavors, compiling edge.js for 2 flavors, and then packaging all this into a Nuget package. This must be run from VS 2013 developer console.

@Alxandr

Not entirely sure how to interpret; but give it your best shot:

image

@Alxandr

Btw. The two js-files you've included, you should add them as embedded resources to the EdgeJS.dll. Easy to do, can help with it if required.

@tjanczuk
Owner

That error came from running build_double.bat from VS 2013 developer console?

The two *.js files must be on disk at runtime for Node runtime to load them. While we can package them as resources, we would need to unpack them to disk at runtime, which I think beats the purpose.

@Alxandr

Yes, it did.
And actually, I think you're wrong. Is it possible to "inject" a C# function into the node-runtime that's callable from the javascript? In that case, you can "hijack" require to run C# code. I know traceur and others do this (though in js) to enable precompiling of code on the fly. This would enable a C# event like

Edge.ResolveModule += (name) => ReturnJsCodeOrNullIfNotFound(name);
@tjanczuk
Owner

Yes of course, once you rig require you can load modules from wherever you want, including resources. I've done my share of rigging require in the past. I don't think the added complexity is worth the benefit of not having 2 extra files on disk. What is the problem with having these files in the first place? "Content" files are standard Nuget concept, we are not innovating here.

Regarding the build error, can you narrow it down to the command that is causing it?

@Alxandr

It's all there in the printscreen (in high-def if you click it). MSBuild fails somewhere, not sure how to get you a better error-message.

Also, the point is that I need to deliver my code (using edge) as a single .dll-file. It can't rely on content-files.

@tjanczuk
Owner

I will see if I can repro it locally with a clean build. It looks like node shared library has not built correctly which makes subsequent build of edge fail.

If you need to deliver your code that depends on Edge as a single dll, you will need to package at least EdgeJs.dll as a resource within it (even if EdgeJs.dll is self-contained). Was that your plan?

@Alxandr

Yeah. I've been using ILMerge for this, though it just hit me I'd need the native libraries too... I'm going to go experiment a bit with recreating the managed parts of your EdgeJs in C# and see what it brings me.

@tjanczuk
Owner

Out of curiosity, why do you require your code to ship as a single dll?

@Alxandr

Amongst other things I'm using it in MSBuild, meaning my dll won't be placed the same place as the starting executable. This will already make edge fail given that it's using the starting assembly as it's base. Another problem is that the framework I'm making (that's using nuget to install packages) requires no content-files and only one .dll per nuget package.

Btw, what are the edge.node files?

@tjanczuk
Owner

Would it help if Edge was using EdgeJs.dll location as the base? It would then at least allow you to bundle Edge.js as a resource in your DLL and unpack to TEMP at runtime.

The edge.node files are native Node.js extensions. On Windows they are really DLLs with a different extension. In this case this is a C++\CLI assembly.

@Alxandr

Yeah. But I'll do some testing to see what's feasible, and get back to you.

Actually, there should be a public property so you can set your own base path, just in case it's needed.

@Alxandr

Also, make your thread a background-thread, so it doesn't keep the application alive if you forget to call Close.

@glennblock
Collaborator
@glennblock
Collaborator
@tjanczuk
Owner

@Alxandr this is more complex than a simple choice of background vs foreground:

  1. In scenarios where one sets up a listener in Node.js we probably want to prevent the application from exiting until the V8 thread terminates (e.g. a piece of code in Node.js calls listener.stop()).
  2. We don't want every invocation of a simple Node.js function from .NET to create a new Node.js thread, set up the V8 VM etc that will terminate right after the function call completes. That would be hugely wasteful.

The Edge.Close API does not "kill" the V8 thread. It merely indicates that CLR no longer has interest in keeping V8 thread alive, which is a weaker statement. If there are any listeners active in Node.js code, the V8 thread will keep running. So if V8 thread is foreground (which is the case now), CLR app won't exit until both Edge.Close was called and the V8 thread terminated gracefully.

One way we could change this is to say Edge.Close has a "kill" semantics. Until it is called the CLR app won't exit. But when it is called V8 thread is terminated even if there are listeners running.

Thoughts?

@tjanczuk
Owner

@glennblock This is unfortunate. I suppose I will call the module Edge.js

@Alxandr

Well, the scenario I just dealt with is where something crashes. My app lost state, and it crashed, however it's kept around cause node still thinks it has stuff to do. Since you don't instantiate "Engines" (or similar, would that be possible?) that are IDisposable I can't use using either.

@tjanczuk
Owner

Good scenario. So perhaps this would work better:

  1. V8 thread is background.
  2. The bootsrap code in JS spins up an interval to keep the same V8 thread alive for consecutive Edge.Func calls.
  3. Edge.Close goes away completely.

In this world one would have to explicitly not exit CLR code if there are Node listeners one cares about.

@glennblock
Collaborator
@dpen2000

@tjanczuk Are you just looking for someone to split the current README into separate MD files in a Docs folder and add links? Or something more than that?

@dpen2000

And: do we want a Docs folder or to use the WIKI some more?

@Alxandr

@dpen2000 @tjanczuk If I get a vote, I vote wiki. When I see "docs" folders I tend to think html and stuff I have to download first to "enjoy". I've learned (sort of) the error of my ways, but I still always check the wiki first.

@glennblock
Collaborator
@glennblock
Collaborator
@tjanczuk
Owner

@dpen2000 @Alxandr I don't necessarily see the need to split the current Readme into multiple pages or documents. I think however it would benefit from refactoring and better structuring as well as new content. I would start with the table of contents section and think how to best structure the content. I am thinking a general section first (everything up to the table of contents), and then two major sections: 1) how to script CLR from Node, 2) how to script Node from CLR (this is new). Most of current content would be subordinate to 1, but some (maybe after light editing) will be relevant to both 1 and 2. New content must be created for 2. I was planning to write some basic starter content for 2 tonight, but it will need more work to bring it up to par with 1. After we have the content and structure in place we can re-assess whether to split this into multiple documents or move to a wiki. If you can help with any or all of that, I will really appreciate it.

@glennblock The entire interop model and 99% of code is the same, I think I would want to continue the brand...

@tjanczuk
Owner

@dpen2000 If you start doing something, please do it against the doubleedge branch.

@Alxandr

@tjanczuk Proof of concept: https://github.com/Alxandr/Edgy/blob/master/Edgy/Program.fs

The Engine class (thin wrapper) implements both IDisposable and a finalizer, so that it get's shut down if garbadge-collected.

@Alxandr

Also, I desperately need a way to call non-async javascript methods, this is the final missing peace.

@tjanczuk
Owner

I just moved V8 to a background thread and removed Edge.Close API so that the process is no longer held up from exiting by the V8 thread. That means Node.js listeners or unfinished function calls between V8 and CLR will not prevent the process from exiting. This is 81d4da0.

I also pushed a new nuget package to the same feed as before. Version number is 0.9.1.

@Alxandr Does this mechanism now satisfy your needs? Do you still need the IDisposable wrapper?

@Alxandr If you are into F#, perhaps you can help validate if @7sharp9 work to enable scripting F# from Node.js works on MacOS and Linux?

@Alxandr Can you create simple async wrappers around your sync methods? I assume the sync methods are non-blocking?

@Alxandr

@tjanczuk Problem with the async wrappers is if they have to return immediately, and at the same time requires a trip back in to node-land.

What I did was setup require in node to call a .NET function with the original require as a parameter. However, when I call that original require I got a deadlock. The solution for now though is to set it up so that the .NET method returns null (or similar) if the default require should handle the request.

@tjanczuk
Owner

Within the async wrapper around original require, can you try wrapping the actual call to the original require with setImmediate? I think that may break the deadlock.

@tjanczuk
Owner

By the way, what are you building if you don't mind me asking?

@Alxandr

Currently a simple preprocessor that runs both less, traceur and react's jsx

@tjanczuk
Owner

And how does edge factor into this?

@Alxandr

Well, all of the engines are written in javascript, and most of them doesn't play nice with existing options such as Jurassic or the MSIE-engine.

@Alxandr

Notice also that the fsharp-project I posted only requires a single .dll, and sorry to say, but I generally don't do linux, and I hate OSX, so I can't really help you with that.

@glennblock
Collaborator
@glennblock
Collaborator
@tjanczuk
Owner
@glennblock
Collaborator
@glennblock
Collaborator
@tjanczuk
Owner
@glennblock
Collaborator
@glennblock
Collaborator
@glennblock
Collaborator
@tjanczuk
Owner

So on that note, do you know what the presence of the install.ps1 in the Edge nuget package will do to the experience on OSX/Linux? Is PowerShell in nuget packages radioactive?

@tjanczuk
Owner

So this is a pretty crazy idea, I wonder if this might work on OSX/Linux. Suppose we ship a nuget package targeting OSX/Linux specifically. Could it contain a tarball with node and edge sources that would be compiled during nuget installation step, similar to what happens on npm install time? In other words, can we use NuGet as a Homebrew?

@tjanczuk
Owner

@jeffhandley your opinion on the previous comment would be appreciated...

@glennblock
Collaborator
@glennblock
Collaborator
@tjanczuk
Owner

I ported tests that made sense from the mocha set with 22d52b8

image

So what remains to be done before pushing this out is a stress test and documentation.

@glennblock
Collaborator
@glennblock
Collaborator
@tjanczuk
Owner

I talked to node core folks in the past about it. They were generally open to entertaining this idea as long as the PR came with a commitment to support this mode with tests and ongoing maintenance. I did not think I would have cycles to make such commitment.

@tjanczuk
Owner

In the course of writing tests I ran into a strange issue, I wonder if anyone has seen something like this before:

This works pefectly well:

Func<object,Task<object>> func;

func(null).ContiueWith(...);

But this kills the process with some fancy 0xC0020001: The string binding is invalid from kernel21.dll (for the same func as above):

Func<object,Task<object>> func;

await func(null);
@tjanczuk
Owner

Looks like my problems were related to http://blog.stephencleary.com/2012/12/dont-block-in-asynchronous-code.html. async/await seems to simplify 80% by 80% and make the remaining 20% some 100% harder.

@unsafecode

I tried out NuGet package today in a simple console application:

public async Task TestEdgeJS()
        {
            try
            {
                var myJS = EdgeJs.Edge.Func(@"
                    var mod = require('../../../node/test-form-validation');
                    return mod.invoke;
                ");

                Console.WriteLine(await myJS("test"));
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

And my NodeJS module (written in TypeScript):

export function invoke(input, cb) {    
    cb(input + '2');
}

When I run the console, I always get an exception, BUT, if everything is correct, the exception message is the actual NodeJS output! Otherwise, a System.AggregateException is raised for real errors. I also managed to use external Node modules for a more complex solution.

Yet still, I can't manage my code to return properly. What am I missing?

@tjanczuk tjanczuk closed this
@tjanczuk
Owner

And to close the loop on the async problem, it appears to have been caused by this: http://blog.stephencleary.com/2012/12/dont-block-in-asynchronous-code.html

@tjanczuk
Owner

@unsafecode The callback function accepts two parameters: an error and the result. Instead of calling cb(input + 2) you should be calling cb(null, input + 2).

@tjanczuk
Owner

Also, I remove the NuGet packages from myget.com. You can now get them from nuget.org. Note the version number is 0.9.0.

@unsafecode

@tjanczuk Thank you very much, now everything's working just fine.

And an even more sincere thank you for all this effort, it is definitely one of the most amazing work I've ever seen :-)

@glennblock
Collaborator
@glennblock
Collaborator

Ok folks, scriptcs-edge is now live: https://github.com/scriptcs-contrib/scriptcs-edge. Currently it only works on Windows (.NET or Mono engines should work). It doesn't support *nix yet as the edge nuget package (which it depends on) only supports Windows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.