tjanczuk / edge

Run .NET and Node.js code in-process on Windows, MacOS, and Linux
http://tjanczuk.github.io/edge
Other
5.41k stars 639 forks source link

add support for scripting F# within a node.js application #32

Closed tjanczuk closed 3 months ago

tjanczuk commented 11 years ago

If you are passionate about F# and would like to help enable running F# and node.js in one process, please consider contributing your time and energy to add F# support to edge.js.

Edge.js project enables in-process interop between node.js and CLR. Currently it supports dynamic compilation and running of C# code inside of a node.js application. It also provides an extensibility mechanism that enables support for other CLR languages. This issue is about extending language support of edge.js to include F#.

To get started, read the overview of the edge.js project as well as the documentation for C# to understand the concepts of edge.js.

Then read about the language extensibility model of edge.js to understand how to support new CLR languages in edge.js.

If you have any questions or comments, please use this issue to discuss. Thank you for your time, effort, and expertise.

7sharp9 commented 11 years ago

Interesting!

It would be great if the interface between the languages was F# native async mechanism rather than the Task oriented one.

tjanczuk commented 11 years ago

Adapting interfaces is the very job of the Edge F# compiler module that solution to this problem requires. Edge.js requires calls to CLR to be normalized to Func<object,Task<object>>. It is the job of the EdgeCompiler author to expose that interface over whatever programming idiom in F# is considered usable by F# programmers. It is likely an F# async expression, but I am not an expert on F#.

7sharp9 commented 11 years ago

I was just referring to being able to do async in native language rather than coercing back to a Task:

fun (o:Object) -> async{return "HelloWorld")

rather than something like:

Func<Object,Task<Object>(fun o -> Async.StartAsTask async{return "HelloWorld"})
7sharp9 commented 11 years ago

Looking at the code in the EdgeCompiler I dare say it could be swapped out to use the F# open source compiler to support the above functionality.

tjanczuk commented 11 years ago

The idea of the language extensibility model in edge.js revolves around normalization to Func<object,Task<object>>. Without this normalization edge.js would have to be aware of mechanisms specific to particular languages it supports, which defies most of the purpose of the extensibility in the first place.

Is coercing to Func<object,Task<object>> particularly problematic for F#? Note that this coercing is not something the developer working with node.js+F# code would need to be aware of. At the application level you can expose whatever programming model is most natural. The coercing happens at the compiler level.

I was hoping building in F# would be quite similar to the edge.js compiler for C#, and I think starting from edge-cs may be a good idea to experiment.

tjanczuk commented 11 years ago

Is there any F# mechanism that allows compilation of script in-memory?

7sharp9 commented 11 years ago

I was just bouncing around ideas. I know that C#s async await involves desugaring via the compiler. The F# compiler does similar things for interoperability with Func.

F# has an interactive console which allows you to code on the fly and excute scripts. There is also work to host the compiler as a service rather than as a dll or involing via command line.

You can probably host F# in the way you suggest. I was thinking in terms of giving F# the most power. F#s async capability predates C#s by 5 or 6 years and allows for greater composability.

Ill dig a bit deeper and continue the discussion ...

tjanczuk commented 11 years ago

Great, looking forward to it!

7sharp9 commented 11 years ago

After thinking about it again and re-reading I think I understand the intent now. At first I was thinking you were implying using the reusing the C# compiler, obviously that wont work because it doesn't know how to deal with F#.

An F# edge compiler seems to be fairly doable, Mono support is a must though for cross platform usage. F# is open source and works on iOS Android, Mac Linux and windows, so it would be able to harness its power from a lot of platforms.

tjanczuk commented 11 years ago

I agree Mono is important. But Mono support first needs to be added to edge.js as part of #3 before it makes sense to add it to any of the compilers. It would be good to enable F# support even before the Mono work happens, similarly to what is there right now for C#.

7sharp9 commented 11 years ago

ack, that means turning on Windows VM!

tjanczuk commented 11 years ago

I feel for you.

panesofglass commented 11 years ago

@7sharp9 I'm updating my fork of @fahadsuhaib's fsi-as-a-dll. It's not fsc, though. I've been digging around looking of late.

tjanczuk commented 11 years ago

Here is another reference implementation for language support, this time for Python: https://github.com/tjanczuk/edge-py. Here is how to use it: https://github.com/tjanczuk/edge#how-to-script-python-in-a-nodejs-application.

It would be nice to get F# next...

py cs js

dfinke commented 11 years ago

Good stuff. Helps me think through what I'm trying to build. I guess IronRuby is just a rename and one reference away?

NickHeiner commented 11 years ago

+1

7sharp9 commented 11 years ago

@tjanczuk Any new on getting this working cross platform?

tjanczuk commented 11 years ago

See #3

7sharp9 commented 11 years ago

Only just getting to this now, writing it as if it will work cross-platform. The edge-fs compiler bit works stand alone, waiting on a new VM build to see if it works on Windows though.

tjanczuk commented 11 years ago

Great! Do you have it somewhere I can spelunk?

panesofglass commented 11 years ago

@7sharp9 want me to test it?

7sharp9 commented 11 years ago

@panesofglass Yep testing would be good, I might be able to get you a binary tomorrow, had a few problems with my WinVM

@tjanczuk Ill get my repo loaded as soon as I can get a Windows sanity check completed.

7sharp9 commented 11 years ago

@tjanczuk I wasn't able to get to the bottom of why calling through the C++ CLI causes the F# compiler to run so slowly, as far as I can see almost 98% of the time is spent in there. I wasn't able to find any evidence of a difference between CLI/CLR performance for C# or F# from a quick scour of the web.

tjanczuk commented 11 years ago

Did you try calling the same logic from regular C# app console to compare? Do you have a small, contained repro of C++\CLI you can share? Like few lines of a C++\CLI exe that invokes the F# compiler? We can try to find some C++\CLI experts to have a look.

7sharp9 commented 11 years ago

Ill try and build one, Ive never use C++ CLI before mind. A C# console app calling the F# compiler yields comparable results to F# to F#.

7sharp9 commented 11 years ago

@tjanczuk Do you see inline lambdas as being a large part of edge's usage or would the dll applications be more common?

tjanczuk commented 11 years ago

In case of C# I see inline async lambdas as the primary way folks write adapter code that calls into pre-existing .NET libraries (either .NET Framework itself, nuget packages, or custom, pre-built class libraries). So I think the usage will be substantial.

When folks are writing sizable .NET code from the ground up to integrate with a Node.js application, I expect the integration will most likely use a pre-compiled CLR library directly, since the adapter code can be compiled into it.

Why do you ask? Are you thinking about punting async lambdas for F#?

7sharp9 commented 11 years ago

punting thats a good expression! :-)

Im not ruling it out just yet, I was just curious. Im working on an alternative compilation method in a branch at the moment. Ill see if I can figure out a simple C++ CLI test harness to figure out the performance issues either way.

7sharp9 commented 11 years ago

@tjanczuk I think it is a hosting issue with either node or the CLI.

The CSharpCodeProvider starts another process (csc.exe) to compile the code and then copy the byte stream via Assembly.Load. If I do a similar thing with the F# compiler then I don't get a massive delay. However thats not currently cross platform - Windows only at the moment. I would like to get to the bottom of the issue though so Ill work on the standalone C++ test harness.

tjanczuk commented 11 years ago

Yes let's try to get to the bottom of this. However given that edge currently only works on Windows, I wonder if we should block releasing F# support on this issue. Perhaps you could compile the F# code out of process just like C# does, and we can fix it up after the issue is understood and resolved?

7sharp9 commented 11 years ago

I can build a win only version, in fact its already done, I've only tested the async lambda support so far though.

tjanczuk commented 11 years ago

Have a look at the edge.csx set of tests for C#. It would probably make sense to have a similar level of functionality (and tests) for F# before going public.

7sharp9 commented 11 years ago

If I use something like this to call the compiler I don't get any performance issues:

int main(array<System::String ^> ^args)
{
    EdgeCompiler^ ec = gcnew EdgeCompiler();
    Dictionary<System::String^,System::Object^>^ parameters = gcnew Dictionary<System::String^,System::Object^>();
    parameters->Add("typeName", "Startup");
    parameters->Add("methodName", "Invoke");                        
    parameters->Add("source", "fun input -> async{return \"node.js welcomes \" + input.ToString()}");
    Stopwatch^ sw = gcnew Stopwatch();
    sw->Start();
    Func<System::Object^,Task<Object^>^>^ res = ec->CompileFunc(parameters);
    Task<Object^>^ t = res->Invoke("F#");
    t->Wait();
    sw->Stop();
    Console::WriteLine("Compiled and Invoked in: {0}ms", sw->Elapsed.TotalMilliseconds);
    return 0;
}

So there must be some issue with the node hosting, maybe memory pressure or something like that, the compiler will produce a lot of garbage building the AST etc.

tjanczuk commented 11 years ago

I wonder if it would make sense to try a different experiment: move the code above to a C++\CLI class library instead of an application, then create a vanilla C++ executable,that loads that library and calls into the logic above. That would most closely mimic what happens in case of node.exe, whereby node.exe is that vanilla C++ executable.

Beyond that, next thing that comes to mind is to break into the sampling profiler in VS to check where exactly that time is spent.

BTW, I can't wait when that scripting syntax you have above will be available ;)

tjanczuk commented 11 years ago

@7sharp9 Nice post! Did you make any progress on the slow activation issue? I talked to the CLR people but they had no immediate hypothesis.

Did you try calling edge.func twice in the same process for different pieces of F# (the literal must differ because edge caches the Func)? Do you see the slow down for both calls, or only the first>

7sharp9 commented 11 years ago

Thanks!

I havent had chance to look at that any more, I switched to the out of process compiler branch for now. The dynamic compilation branch is still in there though.

Ill build some tests around it as it may be a general memory bound issue with the hosting. Im trying to think up a memory bound issue to try out in the C# and python compilers too just to see if there is any performance differences.

tjanczuk commented 11 years ago

Could you write up a short instructions on how to set up the environment to get a simple hello, world going in F#? I would like to experiment a bit.

7sharp9 commented 11 years ago

It should be pretty easy to get going on Windows: (From memory:)

  1. Make sure you have F# installed with your Visual Studio 2012
  2. git clone git@github.com:7sharp9/edge-fs.git
  3. cd edge-fs
  4. I tested the npm install locally with npm install . -g
  5. Navigate to a test project
  6. Install edge-fs into your test project with npm install
  7. create a test node.js file along the lines of:

    var edge = require('edge');
    
    var helloWorld = edge.func('fs', 'fun input -> async{return ".NET welcomes " + input.ToString()}');
    
    helloWorld('JavaScript', function (error, result) {
      if (error) throw error;
      console.log(result);
    });
  8. test with node testfile.js

Ill be writing more documentation as time permits.

tjanczuk commented 11 years ago

Great, thanks. What is the lightweight equivalent of installing F# with VS for pure runtime environments?

Instead of 2-4 one can simply do npm install git://github.com/7sharp9/edge-fs.git -g. Worked for me at least.

tjanczuk commented 11 years ago

@7sharp9 I noticed the compilation executes slowly only for the first compiled function in the node.js app. Subsequent calls to edge.func (with different F# code) are fast.

I ran a sampling profiler on this. It appears that ~90% of the time (inclusive) spent compiling the first F# snippet is in Microsoft.FSharp.Compiler.Driver.getTcImportsFromCommandLine. I am not sure what this piece of code is supposed to do, but there are sure no command line options in our scenario?

7sharp9 commented 11 years ago

There are command lines in both the F# and C# compilers they are wrapped by the API calls. The compiler for C# is invoked with parameters for passed in references and source files etc, if you pass a file via a string it is written to disk and then passed as a parameter to csc.exe. Its exactly the same for the F# compiler. The first call might be slower due the the JIT process.

7sharp9 commented 11 years ago

I would of liked to had more time to work on this but I have been looking at another project recently. Incidentally I would of used Edge.js for the prototype if it had been available for mono.

If you profile the type-1 compiler then compilation phase takes much longer. If do you use the MKI compiler you also have to build the F# compiler as the Dynamic compilation had a bug which was fixed here.

haf commented 9 years ago

Did this ever get completed on mono? Looking to render some reactjs html server-side :)

tjanczuk commented 9 years ago

I don't think it did.

Why do you need F# in Node to render some HTML? Phantomjs does not cut it?

7sharp9 commented 9 years ago

@tjanczuk Because its fun -> :-)

@haf I havent bumped my repo or the node package lately but I think it should be done now.

haf commented 9 years ago

@tjanczuk Well, the whole site is in http://suave.io/ which is F# and I use ReactJS for the front-end. (a very nice combo it turns out), but for some pages I can have some gains from server-side rendering. Also, npm has a lot of high quality packages for sass, coffee script (and assorted transpilers) and in the really long run, it could help to have direct access to the react compiler if I ever got the idea to cut my teeth at om but for F#.

It's also nice to be able to use anything nodejs can provide directly from F#. =) And it's fun.

tjanczuk commented 9 years ago

So it looks like what you are referring to is calling Node from F#, not F# from Node? If that is the case, have you tried the Edge.js nugget package? https://www.nuget.org/packages/Edge.js/

haf commented 9 years ago

Yes, that's right. Thanks, I will absolutely try it!