嗯,虽然的确我也不知道为什么要弄这个……也许只是为了凑文章吧。反正文章质量越来越低了就是了_(:з」∠)_
(水)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);
};
于是,又一套流程完成了。
嘛,大概是这样吧。