agracio / edge-js

Run .NET and Node.js code in-process on Windows, macOS, and Linux

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 ...

image

I call methods from from Node like this ...

image

If I run the Node script with it pointing towards the library compiled by targeting .NET Core 2.2 I get this ...

image

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();