Moving library from .NET Core 1.1 to .NET Core 2.2
ruaumoko opened this issue · comments
First off, I have to say I love EdgeJS. This is a great project and I really hope it keeps going.
I have a precompiled library written in C# and targeting .NET Core 1.1. Methods are called from NodeJS 16.x and all works fine. I'd like to migrate the library to .NET Core 2.2 but I'm struggling to get things to work. I'm pretty sure it should work since the documentations says there is support for NodeJS 16.x. and .NET Core 2.x.
As an example, the source in the library follows this type of pattern ...
I call methods from from Node like this ...
If I run the Node script with it pointing towards the library compiled by targeting .NET Core 2.2 I get this ...
Everything works fine under .NET Core 1.1. Am I missing a step here to make things work under .NET Core 2.2?
Thanks in advance.
(Have put a VS solution here that should (?) reproduce what I'm seeing.)
Giving up on this. Have decided to write my own bridge using the Node Addon API.
I'll eventually set up a separate repo on this but just in case anyone is looking for some pointers to mimic edge using the Node Addon API (with the addon developed in VS2022 using CLI/C++) so that you can call methods in .NET5.0 and higher libraries, here is some code to get you started. It took a while to get running. It may not be elegant (with some clunky object serialization/deserialization) but it does the job ...
// addon.cpp
#include <napi.h>
#include "addon.h"
#include "myobject.h"
namespace MyAddon
{
#pragma managed // Required pragma
//
#pragma unmanaged // Required pragma
Napi::Object CreateFuncObject(const Napi::CallbackInfo& info) {
Napi::Env myEnv = info.Env();
Napi::Object myObj = MyObject::NewInstance(myEnv, info[0].As<Napi::Object>());
return myObj;
}
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
MyObject::Init(env, exports); // Must init object
exports.Set(Napi::String::New(env, "func"), Napi::Function::New(env, CreateFuncObject));
return exports;
}
NODE_API_MODULE(hello, InitAll)
}
// myobject.h
#include <napi.h>
namespace MyAddon
{
class MyObject : public Napi::ObjectWrap<MyObject> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
static Napi::Object NewInstance(Napi::Env env, Napi::Value arg);
MyObject(const Napi::CallbackInfo& info);
private:
Napi::Value Run(const Napi::CallbackInfo& info);
Napi::Value RunAsync(const Napi::CallbackInfo& info);
std::string assemblyPath;
std::string typeName;
std::string methodName;
};
}
// myobject.cpp
#include "myobject.h"
#include <napi.h>
#include <msclr/marshal_cppstd.h> // Required for marshalling strings between managed and unmanaged
namespace MyAddon
{
using namespace System;
using namespace System::Dynamic;
using namespace System::Reflection;
using namespace System::Text::Json;
#pragma managed
static Object^ Deserialize(std::string jsonString)
{
// Convert std::string to System::String
String^ jsonStringAsSystemString = gcnew String(jsonString.c_str());
// Deserialize object and return
ExpandoObject^ obj = JsonSerializer::Deserialize<ExpandoObject^>(jsonStringAsSystemString, gcnew JsonSerializerOptions());
return obj;
}
static std::string Serialize(Object^ obj)
{
// Serialize object to JSON
String^ jsonStringIn = JsonSerializer::Serialize<Object^>(obj, gcnew System::Text::Json::JsonSerializerOptions());
// Convert System:String to std:string
return msclr::interop::marshal_as<std::string>(jsonStringIn);
}
static std::string InvokeMethod(std::string assemblyPath, std::string typeName, std::string methodName) { // Must be std:string arguments to cross managed/unmanaged
// Reference to assembly
System::String^ myAssemblyPathAsSystemString = gcnew String(assemblyPath.c_str());
Assembly^ myAssembly = Assembly::LoadFrom(myAssemblyPathAsSystemString);
// Referenc to type
System::String^ myTypeNameAsSystemString = gcnew String(typeName.c_str());
Type^ myType = myAssembly->GetType(myTypeNameAsSystemString);
// Reference to function
System::String^ myMethodNameNameAsSystemString = gcnew String(methodName.c_str());
//Invoke function
array<System::Object^>^ myArgs = gcnew array<System::Object^>(0);
Object^ myObj = myType->InvokeMember(myMethodNameNameAsSystemString, BindingFlags::InvokeMethod, nullptr, myType, myArgs);
return Serialize(myObj);
}
static std::string InvokeMethod(std::string assemblyPath, std::string typeName, std::string methodName, std::string payload) { // Must be std:string argumnets to cross managed/unmanaged
// Reference to assembly
System::String^ myAssemblyPathAsSystemString = gcnew String(assemblyPath.c_str());
Assembly^ myAssembly = Assembly::LoadFrom(myAssemblyPathAsSystemString);
// Referenc to type
System::String^ myTypeNameAsSystemString = gcnew String(typeName.c_str());
Type^ myType = myAssembly->GetType(myTypeNameAsSystemString);
// Reference to function
System::String^ myMethodNameNameAsSystemString = gcnew String(methodName.c_str());
//Invoke function
array<System::Object^>^ myArgs = gcnew array<System::Object^>(1);
myArgs[0] = Deserialize(payload);
Object^ myObj = myType->InvokeMember(myMethodNameNameAsSystemString, BindingFlags::InvokeMethod, nullptr, myType, myArgs);
return Serialize(myObj);
}
#pragma unmanaged
class Worker : public Napi::AsyncWorker {
public:
Worker(std::string assemblyPath, std::string typeName, std::string methodName,
std::string payload, Napi::Function& callback)
: assemblyPath(assemblyPath), typeName(typeName), methodName(methodName), payload(payload), Napi::AsyncWorker(callback), myMethod() {}
~Worker() {}
// Executed inside the worker-thread.
// It is not safe to access JS engine data structure
// here, so everything we need for input and output
// should go on `this`.
// It's absolutely essential that the Execute method makes NO Node-API calls. This means that the Execute method has
// no access to any input values passed by the JavaScript code.
void Execute() {
myMethod = InvokeMethod(this->assemblyPath, this->typeName, this->methodName, this->payload);
}
// Executed when the async work is complete
// this function will be run inside the main event loop
// so it is safe to use JS engine data again
void OnOK() {
const std::string resultStringified = myMethod;
// Convert std::string to Napi::String
Napi::Env myEnv = this->Env();
Napi::String resultStringifiedAsNapiString = Napi::String::New(myEnv, resultStringified);
// Get references to global function
Napi::Object json = myEnv.Global().Get("JSON").As<Napi::Object>();
Napi::Function parse = json.Get("parse").As<Napi::Function>();
// Parse string and return object
Napi::Value myError = myEnv.Undefined();
Napi::Object myResult = parse.Call(json, { resultStringifiedAsNapiString }).As<Napi::Object>();
// Return object through callback
Callback().Call({ myError, myResult });
}
void OnError(const Napi::Error& error) {
HandleError(error.Message());
};
void HandleError(std::string msg) {
Napi::Env myEnv = this->Env();
Napi::Object myError = Napi::Object::New(myEnv);
myError.Set(Napi::String::New(myEnv, "msg"), Napi::String::New(myEnv, msg));
Napi::Value myResult = myEnv.Undefined();
// Return object through callback
Callback().Call({ myError, myResult });
}
private:
std::string assemblyPath;
std::string typeName;
std::string methodName;
std::string payload;
std::string myMethod;
};
Napi::Object MyObject::Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(
env, "MyObject", { InstanceMethod("run", &MyObject::Run),
InstanceMethod("runAsync", &MyObject::RunAsync)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("MyObject", func);
return exports;
}
MyObject::MyObject(const Napi::CallbackInfo& info) : Napi::ObjectWrap<MyObject>(info) {
Napi::Object myConfigObj = info[0].As<Napi::Object>();
this->assemblyPath = myConfigObj.Get("assembly").As<Napi::String>().Utf8Value();
this->typeName = myConfigObj.Get("typeName").As<Napi::String>().Utf8Value();
this->methodName = myConfigObj.Get("methodName").As<Napi::String>().Utf8Value();
};
Napi::Object MyObject::NewInstance(Napi::Env env, Napi::Value arg) {
Napi::EscapableHandleScope scope(env);
Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New({ arg });
return scope.Escape(napi_value(obj)).ToObject();
}
Napi::Value MyObject::Run(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
std::string myAssemblyPath = this->assemblyPath;
std::string myTypeName = this->typeName;
std::string myMethodName = this->methodName;
Napi::Object myPayload = info[0].As<Napi::Object>();
// Get references to global function
Napi::Object json = env.Global().Get("JSON").As<Napi::Object>();
Napi::Function parse = json.Get("parse").As<Napi::Function>();
Napi::Function stringify = json.Get("stringify").As<Napi::Function>();
// Parse payload
Napi::String myStringifiedPayload = stringify.Call(json, { myPayload }).As<Napi::String>();
// Convert Napi::String to std:string
std::string myStringifiedPayloadAsStdString = myStringifiedPayload.Utf8Value();
std::string myResult = InvokeMethod(myAssemblyPath, myTypeName, myMethodName, myStringifiedPayloadAsStdString);
return Napi::String::New(env, myResult);
}
Napi::Value MyObject::RunAsync(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
std::string myAssemblyPath = this->assemblyPath;
std::string myTypeName = this->typeName;
std::string myMethodName = this->methodName;
Napi::Object myPayload = info[0].As<Napi::Object>();
Napi::Function myCallback = info[1].As<Napi::Function>();
// Get references to global function
Napi::Object json = env.Global().Get("JSON").As<Napi::Object>();
Napi::Function parse = json.Get("parse").As<Napi::Function>();
Napi::Function stringify = json.Get("stringify").As<Napi::Function>();
// Parse payload
Napi::String myStringifiedPayload = stringify.Call(json, { myPayload }).As<Napi::String>();
// Convert Napi::String to std:string
std::string myStringifiedPayloadAsStdString = myStringifiedPayload.Utf8Value();
// Create woker and queue
Worker* myWorker = new Worker(myAssemblyPath, myTypeName, myMethodName, myStringifiedPayloadAsStdString, myCallback);
myWorker->Queue();
return info.Env().Undefined();
}
}
// myclass.cs
using System.Threading;
namespace MyLibrary
{
public class MyClass
{
public static object MyLongRunningMethod()
{
Thread.Sleep(10000);
return new { number = 1000 };
}
public static object MyLongRunningMethod(dynamic payload)
{
Thread.Sleep(10000);
return payload;
}
}
}
// test.js
var interop = require('bindings')('addon');
var myFunc = interop.func({
assembly: "library.dll",
typeName: "MyLibrary.MyClass",
methodName: "MyLongRunningMethod"
});
console.log("Calling function async");
myFunc.runAsync({ id: 1, numbers: [1.0, 2.0, 3.0] }, (error, result) => {
console.log("Async result ->");
console.log(error);
console.log(result);
});
console.log("Waiting ...")
process.stdin.resume();