Search code examples
c#c++dllunity-game-enginecom

What is the best way to call a native DLL, from Unity3d?


Hi Everyone I am having some issues on unity3d pro, perhaps someone that has work with DLL native plugins can shed some light as to what I am doing wrong.

To begin I will explain what my goal is. I have a COM object from Siemens PLCSim software, I've gotten that to work fine with Visual Studio. Below is the test code.

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public S7PROSIMLib.S7ProSim PLCSimConn = new S7PROSIMLib.S7ProSim();

        public Form1()
        {
            InitializeComponent();
        }

        private void button_Connect_Click(object sender, EventArgs e)
        {
            PLCSimConn.Connect();
            label_CPUState.Text = PLCSimConn.GetState();
            label_ScanMode.Text = PLCSimConn.GetScanMode().ToString();
        }
    }
}

I created a unity3d project to test the same. I imported the dll to my assets, and I am able to call the methods and instances of S7PROSIMLibfrom my c# script. COM Api here, https://cache.industry.siemens.com/dl/files/855/1139855/att_29424/v1/S7WSPSCB.pdf

using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
using S7PROSIMLib;

public class TestNative : MonoBehaviour {

    public S7ProSimClass ps; 

    // Use this for initialization
    void Start () {

        ps = new S7ProSimClass ();
        ps.Connect ();
        Debug.Log (ps.GetState());

    }

}

Now at run time I get the following:

COMException

System.Runtime.InteropServices.Marshal.ThrowExceptionForHR (Int32 errorCode) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.InteropServices/Marshal.cs:1031)
System.__ComObject.Initialize (System.Type t) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/__ComObject.cs:103)
(wrapper remoting-invoke-with-check) System.__ComObject:Initialize (System.Type)
Mono.Interop.ComInteropProxy.CreateProxy (System.Type t) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/Mono.Interop/ComInteropProxy.cs:108)
System.Runtime.Remoting.RemotingServices.CreateClientProxyForComInterop (System.Type type) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Remoting/RemotingServices.cs:588)
System.Runtime.Remoting.Activation.ActivationServices.CreateProxyForType (System.Type type) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Remoting.Activation/ActivationServices.cs:234)
TestNative.Start () (at Assets/Scripts/TestNative.cs:13)

I've watched the unity3d plugin tutorial https://www.youtube.com/watch?v=DfRYLwG1Bug which doesn't call a third party DLL.

I have read the following: https://msdn.microsoft.com/en-us/library/ms973872.aspx which has a lot of good information on unmanaged and managed DLL's.

Based on the following snippet from the link above:

Calling COM APIs There are two ways to call COM components from managed code: through COM interop (available in all managed languages) or through C++ interop (available in C++). For calling OLE Automation-compatible COM components, the suggestion is to use COM interop. The CLR will take care of COM component activation and parameter marshaling.

For calling COM components based on Interface Definition Language (IDL), the suggestion is to use C++ interop. The C++ layer can be very thin and the rest of the managed code can be written in any managed language. COM interop relies on information from type libraries to make correct interop calls, but type libraries typically do not contain all the information present in IDL files. Using C++ interop solves this problem by allowing direct access to these COM APIs.

I believe I already did the COM interop shown on my unity c# script above. And that didn't work. My other option is through C++ interop, but I haven't found any example online. Most of the examples are simple without calling a COM object or any other third party DLL. I would appreciate if someone can direct me in the right way. Basically it comes down to what is the best way to go about this, with minimal re writing of methods? Thank you!

Edit1: I watched this video on youtube from someone getting it to work, but I haven't been able to get a response from him. At least I know that it should work on unity3d. https://www.youtube.com/watch?v=EGFMjUJN7ZU

Edit2: Unity3d - Failed to load 'Assets/Plugins/QCARWrapper.dll' Will try with 32bit Editor as this post suggests.


Solution

  • For anyone that might need help with this. Siemens PLCSim COM type library only works with 32bit Unity Editor.

    The solution was actually quite easy after lots of trial and error, and many more reading through the forums and what not. I used tlbimp.exe to generate a type library DLL, then I placed that into the assets/plugins folder also note on unity3d editor properties that the new DLL is treated as managed code. Note that this particular DLL doesn’t work on 64bit Unity Editor and it threw a COMException. However the new type library DLL works fine in 32bit Unity Editor, I imagine that somewhere in the DLL there is an instruction specifying to work only in 32bit.

    I documented everything on a repository here https://github.com/fredz0003/myS7ProSimLib I also placed the managed DLL for future use so you don't go to the trouble I went.

    Below is a sample code:

    using UnityEngine; using myS7ProSimLib;
    
    public class TestNative : MonoBehaviour {
    
        /*
        * Used tlbimp.exe to generate library DLL
        * place new generated DLL on assets/plugins
        * also note that the DLL is treated as managed code
        */
        public S7ProSimClass ps;
        public bool input_0_0;
    
    
        void Start () {
            ps = new S7ProSimClass();
    
            ps.Connect();
            print("State " + ps.GetState());
            ps.SetScanMode(ScanModeConstants.ContinuousScan);
    
            // Here we pass the ref as an obj, since WriteInputPoint method
            // can take bit, word, dwords, as addresses ref obj can take the
            // for of bool, int, float, etc.
            object refInput0_0 = input_0_0;
    
            ps.WriteInputPoint(0, 0, ref refInput0_0);  }    }