C#与JavaScript的共生

zsx in 记录整理 / 3 / 6920

嗯,虽然的确我也不知道为什么要弄这个……也许只是为了凑文章吧。反正文章质量越来越低了就是了_(:з」∠)_

(水)Compile C# To JavaScript

某些时候总有一些奇怪的需求的。比如我自己最近就有了一个在浏览器里调用某C#方法的需求。于是就直接找到一个可以把C#翻译为JavaScript的Compiler:JSIL:https://github.com/sq/JSIL。这玩意的原理呢,是从反编译被编译成可执行程序的.NET程序,从中取出.NET中间代码并进行分析,再翻译成JavaScript的。正因为此,其翻译成的程序的特点就是体积巨大。一个去除了所有非必须引用,仅有一个入口文件两个函数的DLL能编译出20M的代码。根据编译出的东西看,包括XNA、音频库种种。效率有点低,代码污染有点严重,必须隔离一个窗口。也许挺适合HTML5游戏等场合的吧,如果只是要提取个小函数,可以用其把相关内容转为JavaScript语法然后自己把剩下的依赖库实现一遍比较合适……


回到正题。有个库,叫Edge(https://github.com/tjanczuk/edge)。其能实现.NET CLR语言与Nodejs的互相调用。于是,本着水文章的原则,就大概看了一看实现方法。

Nodejs调用C#

先是看示例:

var helloWorld = edge.func(function () {/*

    async (input) => {

         return ".NET Welcomes " + input.ToString();

     }

*/});

怎么做到的呢?一步一步来吧。

解析C#代码

首先,按照ECMAScript规范(http://www.ecma-international.org/ecma-262/6.0/#sec-function-constructor ):

Return an implementation-dependent String source code representation of func. 

返回这个函数与实现相关的、字符串类型的源代码。那什么又是实现相关呢?

The use and placement of white space, line terminators, and semicolons within the representation String is implementation-dependent.

空格、行终止符、分号的使用和布局是实现相关的。

按照MDN(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString )的理解,大概直接就是返回源代码了吧。(于是上面这串到底有毛用呢?)翻阅v8源代码,也差不多。

Handle<Object> SharedFunctionInfo::GetSourceCode() {

  if (!HasSourceCode()) return GetIsolate()->factory()->undefined_value();

  Handle<String> source(String::cast(Script::cast(script())->source()));

  return GetIsolate()->factory()->NewSubString(

      source, start_position(), end_position());

}

于是,我们就可以愉快地取出字符串了:

    if (typeof options.source === 'function') {

        var match = options.source.toString().match(/[^]*\/\*([^]*)\*\/\s*\}$/);

        if (match) {

            options.source = match[1];

        }

        else {

            throw new Error('If .NET source is provided as JavaScript function, function body must be a /* ... */ comment.');

        }

    }

编译C#代码

接着一个initializeClrFunc,转交给了C++

            assembly = Assembly::UnsafeLoadFrom(System::Text::Encoding::Unicode->GetString(buffer));

            System::Type^ compilerType = assembly->GetType("EdgeCompiler", true, true);

            System::Object^ compilerInstance = System::Activator::CreateInstance(compilerType, false);

            MethodInfo^ compileFunc = compilerType->GetMethod("CompileFunc", BindingFlags::Instance | BindingFlags::Public);

            if (compileFunc == nullptr) 

            {

                throw gcnew System::InvalidOperationException(

                    "Unable to access the CompileFunc method of the EdgeCompiler class in the edge.js compiler assembly.");

            }



            System::Object^ parameters = ClrFunc::MarshalV8ToCLR(options);

            System::Func<System::Object^,Task<System::Object^>^>^ func = 

                (System::Func<System::Object^,Task<System::Object^>^>^)compileFunc->Invoke(

                    compilerInstance, gcnew array<System::Object^> { parameters });

            result = ClrFunc::Initialize(func);

调用了其早已编译好的edge-cs.dll,得到其EdgeCompiler::CompileFunc方法后进行invoke调用。

然后,使用EdgeCompiler::TryCompile,借助Microsoft.CSharp.CSharpCodeProvider尝试编译C#代码。

        Dictionary<string, string> options = new Dictionary<string, string> { { "CompilerVersion", "v4.0" } };

        CSharpCodeProvider csc = new CSharpCodeProvider(options);

        CompilerParameters parameters = new CompilerParameters();

        parameters.GenerateInMemory = true;

        parameters.IncludeDebugInformation = debuggingEnabled;

        parameters.ReferencedAssemblies.AddRange(references.ToArray());

        parameters.ReferencedAssemblies.Add("System.dll");

        parameters.ReferencedAssemblies.Add("System.Core.dll");

        parameters.ReferencedAssemblies.Add("Microsoft.CSharp.dll");

        if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("EDGE_CS_TEMP_DIR")))

        {

            parameters.TempFiles = new TempFileCollection(Environment.GetEnvironmentVariable("EDGE_CS_TEMP_DIR"));

        }

        CompilerResults results = csc.CompileAssemblyFromSource(parameters, source);

Invoke调用

编译完成后,尝试从编译后的Assembly动态加载其Startup::Invoke方法

        Type startupType = assembly.GetType((string)parameters["typeName"], true, true);

        object instance = Activator.CreateInstance(startupType, false);

        MethodInfo invokeMethod = startupType.GetMethod((string)parameters["methodName"], BindingFlags.Instance | BindingFlags.Public);

        if (invokeMethod == null)

        {

            throw new InvalidOperationException("Unable to access CLR method to wrap through reflection. Make sure it is a public instance method.");

        }



        // create a Func<object,Task<object>> delegate around the method invocation using reflection

        Func<object,Task<object>> result = (input) => 

        {

            return (Task<object>)invokeMethod.Invoke(instance, new object[] { input });

        };

返回这个Task以后,回到C++来,到

v8::Local<v8::Function> ClrFunc::Initialize(System::Func<System::Object^,Task<System::Object^>^>^ func)

由其对进行调用,包装一层callback后丢回Nodejs,大功告成。


C#调用Nodejs

        // Find the entry point with `dumpbin /exports node.exe`, look for Start@node

        [DllImport("node.dll", EntryPoint = "#928", CallingConvention = CallingConvention.Cdecl)]

        static extern int NodeStart(int argc, string[] argv);

首先,NuGet下载的node.dll,实际上是修改后编译过的node.exe,也就是Nodejs主程序本身。所以,与Nodejs跑C#依赖CLR不同的是,C#跑Nodejs不需要依赖于宿主机的Nodejs。

包装函数

翻阅代码。这里调用node.dll的方式,效果等同于执行node double_edge.js。然后通过

waitHandle.WaitOne();

等待这个js进行回调。

这个js里做了什么呢?回答是,它通过Nodejs调用C#的方式,略过了编译过程,直接调用EdgeJs.Dll的EdgeJs.Edge.InitializeInternal方法,把包装过的compileFunc交给C#。

var initialize = edge.func({

    assemblyFile: __dirname + '\\..\\EdgeJs.dll',

    typeName: 'EdgeJs.Edge',

    methodName: 'InitializeInternal'

});

initialize(compileFunc, function (error, data) {

    if (error) throw error;

});

此时,InitializeInternal负责把JavaScript包装成C#的Task,然后停止等待。

        public Task<object> InitializeInternal(object input)

        {

            compileFunc = (Func<object, Task<object>>)input;

            initialized = true;

            waitHandle.Set();



            return Task<object>.FromResult((object)null);

        }

接着,C#回过头,调用Task,注入代码后Wait for callback。js里用eval进行执行,执行完后重新callback。

var compileFunc = function (data, callback) {

    var wrapper = '(function () { ' + data + ' })';

    var funcFactory = eval(wrapper);

    var func = funcFactory();

    if (typeof func !== 'function') {

        throw new Error('Node.js code must return an instance of a JavaScript function. '

            + 'Please use `return` statement to return a function.');

    }



    callback(null, func);

};

于是,又一套流程完成了。


嘛,大概是这样吧。

如果本文对你有帮助,你可以用支付宝支持一下:

Alipay QrCode
qiukong at 2015/12/10[回复]
不知是不是我撸多了的缘故,看你博客都是虚的……
zsx at 2015/12/13[回复]
修好了_(:з」∠)_ https://github.com/zsxsoft/blog.zsxsoft.com/commit/0e7167ecfed6836c0bf24ef6c4525d913b6eb688