plsql-developer-plugin-net icon indicating copy to clipboard operation
plsql-developer-plugin-net copied to clipboard

PLSQLDev Plugin in C# - using out parameter get system error

Open freundlee opened this issue 7 years ago • 3 comments

Hi,

I just follow instruction to call function IdeGetConnectionInfo. However it returns an system exception: sshot-1

No idea why this happen. Can anyone help me?

thx, Alan

I--------------------------------------------------------------------------------------------

Receiving a string as an out argument:

// C++ signature: void IDE_GetConnectionInfo(char **Username, char **Password, char **Database); // Delphi signature: procedure IDE_GetConnectionInfo(var Username, Password, Database: PChar);

// Declaration public delegate void IdeGetConnectionInfo(out string username, out string password, out string db); private IdeGetConnectionInfo getConnectionInfoCallback;

// Usage // IMPORTANT: Allocate out string before call. string user = ""; string password = ""; string db = ""; getConnectionInfoCallback(out user, out password, out db);

freundlee avatar Oct 15 '18 11:10 freundlee

Hi, Alan (@freundlee), My apologies for the late response -- the project now is kind of abandoned by me simply due to lack of spare time. Hope will return to it soon. First, I need a little bit more info:

  1. Your OS (Win7, Win10 etc)
  2. PL/SQL Developer version number (10, 11, 12...)

Sorry, my PC is old and slow, and I can't spin a virtual machine on in to try on different Win versions. For now, I give you a code, which compiles and works on my Win7 32bit machine, PL/SQL Dev 10. What you have to do:

  1. Create a project in Visual Studio.
  2. Add NuGet package "Robert Giesecke Unmanaged exports" to project.
  3. Add System.Windows.Forms reference to project (in solution explorer Add > References ...)
  4. Be sure you set x64, when you build the project.
  5. After successful build, copy the dll to plugins directory.
  6. Restart PL/SQL Developer.

Plugin creates a menu "Get connection info demo" under Tools. By clicking it, it call GetConnectionInfo and shows user, password and db, to which you are currently connected.

I'm waiting for your response, if it worked to you.

using System;
using RGiesecke.DllExport;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace GetConnectionInfoExample
{

    public delegate void IdeGetConnectionInfo(out string username, out string password, out string db);

    public class GetConnectionInfoExamplePlugin
    {
        private static IdeGetConnectionInfo getConnectionInfoCallback;

        private const string PLUGIN_NAME = "Sample GetConnectionInfo plug-in";

        private const int PLUGIN_MENU_INDEX = 1;

        private static GetConnectionInfoExamplePlugin me;
        private int pluginId;

        private GetConnectionInfoExamplePlugin(int id)
        {
            pluginId = id;
        }

        #region DLL exported API
        [DllExport("IdentifyPlugIn", CallingConvention = CallingConvention.Cdecl)]
        public static string IdentifyPlugIn(int id)
        {
            if (me == null)
            {
                me = new GetConnectionInfoExamplePlugin(id);
            }
            return PLUGIN_NAME;
        }

        [DllExport("RegisterCallback", CallingConvention = CallingConvention.Cdecl)]
        public static void RegisterCallback(int index, IntPtr function)
        {

            getConnectionInfoCallback = (IdeGetConnectionInfo)Marshal.GetDelegateForFunctionPointer(function, typeof(IdeGetConnectionInfo));
        }

        [DllExport("OnMenuClick", CallingConvention = CallingConvention.Cdecl)]
        public static void OnMenuClick(int index)
        {
            if (index == PLUGIN_MENU_INDEX)
            {
                string user = "";
                string password = "";
                string db = "";
                getConnectionInfoCallback(out user, out password, out db);
                MessageBox.Show("User: " + user + "\nPassword: " + password + "\nDB: " + db, "Connection info");

            }
        }

        [DllExport("CreateMenuItem", CallingConvention = CallingConvention.Cdecl)]
        public static string CreateMenuItem(int index)
        {
            if (index == PLUGIN_MENU_INDEX)
            {
                return "Tools / Get connection info demo";
            }
            else
            {
                return "";
            }
        }

        [DllExport("About", CallingConvention = CallingConvention.Cdecl)]
        public static string About()
        {
            return PLUGIN_NAME;
        }
        #endregion
    }
}

aniskop avatar Nov 18 '18 20:11 aniskop

Hello,

This fix doesn't work, it containts also a bug, the RegisterCallback isn't implemented correctly, it should look like:

if (index == CALLBACK_CONNECTION_INFO ) //12 { callbackGetConnectionInfo = (IdeGetConnectionInfo)Marshal.GetDelegateForFunctionPointer(function, typeof(IdeGetConnectionInfo)); }

If you apply this pl/sql developer crashes....

I think the problem are the out parameters, when I change them to IntPtr:

public delegate void IdeGetConnectionInfo(out IntPtr username, out IntPtr password, out IntPtr db);

IntPtr user; IntPtr password; IntPtr db; callbackGetConnectionInfo(out user, out password, out db);

Now pl/sql developer doesn't crashes, and this message: MessageBox.Show("TEST: " /*+ test */ + "\nUser: " + user.ToString() + "\nPassword: " + password.ToString() + "\nDB: " + db.ToString(), "Connection info");

Shows this information:

TEST: User: 179695408 Password: 179673712 DB: 179699536

I am not sure if IntPtr is the right datatype, when I use char pl/sql developer alse doesn't crash.

Anyone know what the correct datatype is and how to convert it to a string?

goosjuh avatar Apr 11 '19 09:04 goosjuh

Hi, Oops, you're right @goosjuh – I've deleted if in the RegisterCallback.

It seems, that you are facing the same issue I've seen with the string return values.

Short answer You need explicitly marshal (or cast, in other words) IntPtr to string. Just call Marshal.PtrToStringAnsi on each of out parameters, for instance, string username = Marshal.PtrToStringAnsi(user). Please refer to Passing and receiving strings for details. Although that page describes usage of this technique for marshalling return value, this should work for out parameters as well.

Longer answer IntPtr is (at least should be) the right datatype – it is a pointer to anything in the unmanaged memory area e.g. outside .NET CLR. In your case this is a string. Your plug-in's code lives inside the .NET CLR (managed memory area), thus knows nothing about the outside world (anything which is located in the unmanaged memory).

>>> Without marshalling, the value behind IntPtr means nothing to your C# code. <<<

This you've nicely illustrated: 179695408 is a decimal value of the address where user parameter value is stored. Usually, .NET does implicit marshalling e.g. casts IntPtr to a meaningful value behind the scenes – you don't need to do anything. This works fine with long, int (string on my older 32-bit machine).

However, this does not work with booleans (needs [MarshalAs(UnmanagedType.Bool)]) and, it appears, it does not work with strings (I don't know the reasons). So you need to do an explicit marshalling by calling Marshal.PtrToStringAnsi – this should work.

Let me know if that solved the issue.

See also:

aniskop avatar Apr 15 '19 18:04 aniskop