Экспорт функции C из C # и использование ее в VBA - PullRequest
0 голосов
/ 27 мая 2011

Я использую решение от Robert Giesecke http://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports для экспорта функций из управляемого кода в неуправляемый код. Решение работает довольно хорошо, но есть проблема с использованием решения с офисом (Excel).

Я пытался разработать DLL, которая

  • подключается к SQLServer
  • с использованием SQLAuthentication
  • передавая имя базы данных
  • прохождение SQL-оператора
  • и возвращая результат

чтобы пользователь DLL не мог видеть пароль, я знаю, что это можно сделать с помощью специальных инструментов. Этот способ достаточно для нашего требования.

код в C #:

using System;
using System.Collections.Generic;
using System.Text;
using RGiesecke.DllExport;
using ADODB;
using System.Xml;
using System.IO;
using System.Security.Cryptography;
using System.Runtime.InteropServices; 
using System.Windows.Forms;

namespace SqlConRVT
{
public static class SqlConRVT 
{ 
    [DllExport("SqlConRVT", CallingConvention = CallingConvention.StdCall)] 
    [return: MarshalAs(UnmanagedType.IDispatch)] 
    public static Object OpenRecordset ([MarshalAs(UnmanagedType.AnsiBStr)] string databaseName, 
    [MarshalAs(UnmanagedType.AnsiBStr)] string commandText) 
    {
    if (String.IsNullOrEmpty( databaseName)) throw new ArgumentNullException("databaseName"); 
    if (String.IsNullOrEmpty( commandText)) throw new ArgumentNullException("commandText");
    try 
    { 
        var connection = new ADODB.Connection();
        var intConnectionMode = (int) ConnectModeEnum.adModeUnknown;
        var username = Crypto.DecryptMessage("XEj0PC2lMIs=", "FinON");
        var password = Crypto.DecryptMessage("7YIDPO7eBoFAhskAX6JGAg==", "FinON");
        connection.Open("Provider='SQLOLEDB';Data Source='PETER-PC\\SQLEXPRESS'; Initial Catalog='" + databaseName + "';", username, password, intConnectionMode);
        var rs = new Recordset();
        rs.Open(commandText, connection, CursorTypeEnum.adOpenForwardOnly, LockTypeEnum.adLockOptimistic, -1);
        return rs; 
    } 
    catch (Exception ex) 
    { 
        // an exception in a DLL will most likely kill the excel process 
        // we really dont want that to happen 
        MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); 
        return null; 
    } 
} 
} 

public partial class Crypto
{
    public static string DecryptMessage(string encryptedBase64, string password)
    {
        TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider();
        des.IV = new byte[8];
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, new byte[0]);
        des.Key = pdb.CryptDeriveKey("RC2", "MD5", 128, new byte[8]);
        byte[] encryptedBytes = Convert.FromBase64String(encryptedBase64);
        MemoryStream ms = new MemoryStream(encryptedBase64.Length);
        CryptoStream decStream = new CryptoStream(ms, des.CreateDecryptor(), CryptoStreamMode.Write);
        decStream.Write(encryptedBytes, 0, encryptedBytes.Length);
        decStream.FlushFinalBlock();
        byte[] plainBytes = new byte[ms.Length];
        ms.Position = 0;
        ms.Read(plainBytes, 0, (int)ms.Length);
        decStream.Close();
        return Encoding.UTF8.GetString(plainBytes);
    }
}
}

Мой код в VBA:

Declare Function SqlConRVT Lib _
"C:\Users\Administrator\Documents\Visual Studio 2008\Projects\SqlConRVT\SqlConRVT\bin\Debug\x86  \SqlConRVT.dll" (ByVal databaseName As String, ByVal commandText As String) As Object

Sub SQLCon()
Dim x As Object
x = SqlConRVT("Adressen", "Select * from tblAdressen")
End Sub

В C # DLL и во всех клиентских приложениях я ссылаюсь на «Microsoft ActiveX Data Object 2.8 Library».

Я пытался использовать экспортированную 64-битную DLL с C #, работает нормально. Я пытался использовать экспортированную 64-битную DLL в качестве статического класса с C #, отлично работает. Я пытался использовать экспортированную 32-битную DLL с VB6, приложение вылетает. Я пытался использовать экспортированную 32-битную DLL с VBA (Excel), приложение вылетает.

Я проверил существование экспортированной функции в 32-битной DLL с помощью обходчика зависимостей.

Почему я не могу использовать 32-битную DLL с офисом (Excel)?


Конечно, у меня 32-битный Office!

Ваш "упрощенный пример" работает отлично, класс возвращен правильно!

Я сократил свой пример:

using System;
using System.Collections.Generic;
using System.Text;
using RGiesecke.DllExport;
using ADODB;
using System.Xml;
using System.IO;
using System.Security.Cryptography;
using System.Runtime.InteropServices;
using System.Windows.Forms;

[ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual)]

static class SqlConRVT
{
    [DllExport(CallingConvention = CallingConvention.StdCall)]
    [return: MarshalAs(UnmanagedType.IDispatch)]
    //[return: MarshalAs(UnmanagedType.I4)]
    //[return: MarshalAs(UnmanagedType.AnsiBStr)]

    static Object GetNewObject([MarshalAs(UnmanagedType.AnsiBStr)] String databaseName,
    [MarshalAs(UnmanagedType.AnsiBStr)] String commandText)
    {
        var test = new StreamReader("C:\\lxbu.log");
        return test;
        //var rs = new Recordset();
        //return rs;
        //int A = 1;
        //return A;
        //String A = commandText;
        //return A;
     }
 }

Мой код в VBA:

Declare Function GetNewObject Lib "C:\Users\Administrator\Documents\Visual Studio 2008\Projects\An\An\bin\Debug\x86\An.dll" (ByVal databaseName As String, ByVal commandText As String) As Object

Sub An1()
Dim x As Object
Set x = GetNewObject("Adressen", "Select * from tblAdressen")
End Sub

Если я попытаюсь вернуть int-значение -> работает правильно! Если я пытаюсь вернуть строковое значение -> работает правильно! Если я пытаюсь вернуть объект (например, объект набора записей или объект потокового чтения), происходит сбой Excel? Там должна быть маленькая глупая ошибка!


Спасибо, Роберт, - как всегда, твой код идеален! Я могу видеть содержимое объекта Streamreader, если я использую следующий код в VBA

MsgBox instance.ReadtoEnd()

и результат:

"abc Äö ~ éêè @dkfjf -> Добавлено для VBA"

Проблема определенно в ADODB.connection !!!!!

    [DllExport(CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.IDispatch)]
static Object GetNewObject([MarshalAs(UnmanagedType.LPStr)] String databaseName, [MarshalAs(UnmanagedType.LPStr)] String commandText)
{
    //if (String.IsNullOrEmpty(databaseName)) throw new ArgumentNullException("databaseName");
    //if (String.IsNullOrEmpty(commandText)) throw new ArgumentNullException("commandText");
    {
        var connection = new ADODB.Connection();
        //var rs = new Recordset();
        StreamReader sr = new StreamReader("C:\\lxbu.log");
        //var intConnectionMode = (int)ConnectModeEnum.adModeUnknown;
        //var username = "...";
        //var password = ".........";
        //connection.Open("Provider='SQLOLEDB';Data Source='PETER-PC\\SQLEXPRESS'; Initial Catalog='" + databaseName + "';", username, password, intConnectionMode);
        //rs.Open(commandText, connection, CursorTypeEnum.adOpenForwardOnly, LockTypeEnum.adLockOptimistic, -1);
        return sr;
    }
}

Если я использую "var connection = new ADODB.Connection ();" Excel вылетает. Проблема заключается в использовании ADODB в 32-битной DLL (C # и использование 64-битной DLL не проблема). Нет проблем (!!!) с вашим решением!

Ответы [ 3 ]

1 голос
/ 30 мая 2011

Как я спросил вас в нашем почтовом разговоре: вы действительно используете 64-битный офис?
Это очень маловероятно, поэтому я хотел проверить это заранее.

Даже если вы используете 64-битныйОфис, он все еще должен работать.Одна вещь, которую вы должны помнить: когда вы вызываете функцию DLL из VBA, тип передаваемой строки будет LPStr (указатель на Ansi Char).AnsiBStr должен сделать это также.Но определенные вами классы будут использовать BStr, который является стандартным для COM.

Вот упрощенный пример, который не требует ни клиентских библиотек MSSQL, ни ADODB.(Таким образом, меньше точек отказа) Отказ от ответственности: Хотя у меня есть 64-битная Windows, у меня установлен только x86 Office (2007):

[ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual)]
public class Sample
{
   public string Text
   {
      [return: MarshalAs(UnmanagedType.BStr)]
      get;
      [param: MarshalAs(UnmanagedType.BStr)]
      set;
   }

   [return: MarshalAs(UnmanagedType.BStr)]
   public string TestMethod()
   {
      return Text + "...";
   }
}

static class UnmanagedExports
{
   [DllExport(CallingConvention = CallingConvention.StdCall)]
   [return: MarshalAs(UnmanagedType.IDispatch)]
   static Object CreateDotNetObject([MarshalAs(UnmanagedType.LPStr)] String text)
   {
      try
      {
         return new Sample { Text = text };
      }
      catch (Exception ex)
      {
         MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error);
         return null;
      }
   }
}

И это как использовать его из всего, что связано с VBA (например, Excel или Access)

Declare Function CreateDotNetObject Lib "The full path to your assembly or just the assembly if it is accessible from Excel" (ByVal text As String) As Object
Sub test()

  Dim instance As Object

  Set instance = CreateDotNetObject("Test 1")
  Debug.Print instance.Text

  Debug.Print instance.TestMethod

  instance.text = "abc 123" ' case insensitivity in VBA works as expected

  Debug.Print instance.Text
End Sub

Если это работает для вас, мы можем получить оттуда, куда вы хотите пойти.Но было бы важно узнать, какая у вас версия для офиса (платформа ЦП) и работает ли этот простой пример first .

1 голос
/ 31 мая 2011

Я снова задам вам несколько вопросов, так как вы как бы уклонились от некоторых вопросов (здесь и по электронной почте ранее), и я не совсем уверен, что вы на самом деле пытались:

  • Вы не установили процессорПлатформа вашего проекта на x86?
  • Если нет, вы выбираете сборку в подпапке x86?
  • Вы пытались использовать UnmanagedType.LPStr для ваших строковых параметров?
    Пока у меня естьЕсли бы у меня был опыт управления / неуправляемого взаимодействия, мне никогда раньше не приходилось работать с VBA, поэтому я не уверен насчет AnsiBStr.

Даже не пытайтесь загрузить что-либо еще, кроме DLL x86, не может работать.
Сделайте себе одолжение, измените CPU Platform вашего проекта на x86 и удалите текущую выходную папку.Я боюсь, что все эти разные версии смешались.После перестройки у вас должен быть только x86, который должен работать очень хорошо.

И чтобы быть уверенным в этом, работайте нормально: попробуйте этот вариант, который является вариантом того, который вы заявили, чтоэто не работает.И пожалуйста , постарайтесь больше ничего не делать.Лучший способ проверить это - новый проект, созданный из моего шаблона, и вставить приведенный ниже код в пример класса экспорта.

C #

[DllExport]
[return: MarshalAs(UnmanagedType.IDispatch)]
static Object CreateDotNetObject([MarshalAs(UnmanagedType.LPStr)] String text)
{
   try
   {
      var testFileName = Path.Combine(Path.GetTempPath(), "VbaTestFile.txt");
      if (!File.Exists(testFileName))
         File.WriteAllText(testFileName, "abc Äö ~éêè @dkfjf", Encoding.UTF8);

      using (var writer = File.AppendText(testFileName))
         writer.WriteLine(text);

      return new StreamReader(testFileName);
   }
   catch (Exception ex)
   {
      MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error);
      return null;
   }
}

VBA

Declare Function CreateDotNetObject Lib "Full path to your assembly" (ByVal text As String) As Object

Sub Test()

  Dim instance As Object

  Set instance = CreateDotNetObject("-> Added fro VBA")
  Debug.Print instance.ReadToEnd()
  instance.Close
End Sub

Что вы видели в ближайшем окне VBA?

1 голос
/ 28 мая 2011

Я не могу ответить на ваш вопрос, но, возможно, используемый вами сторонний код имеет недокументированные ограничения или предположения, которые не работают при работе в Office.

Однако существуют и другие способы экспорта управляемогоAPI для Excel VBA.Я использую следующее решение:

  • Определите набор двойных COM-интерфейсов в IDL для API, который вы хотите предоставить в .NET.Должен быть один основной интерфейс Factory, который можно использовать в качестве основной точки входа из VBA.Фабрика должна иметь возможность прямо или косвенно создавать экземпляры любых объектов, которые вы хотите выставить (описанная ниже методика не позволяет VBA создавать объекты напрямую, поэтому вам нужно делать это с помощью класса Factory).

  • Создайте TypeLib из IDL с помощью MIDL.EXE и используйте TlbImp, чтобы открыть его для .NET

  • Создать проект библиотеки классов .NET, который ссылается на взаимодействие COMсборка, сгенерированная TlbImp, и запись классов, реализующих API.

  • Создание проекта VSTO.В обработчике события ThisWorkbook.Workbook_Open создайте экземпляр основного объекта Factory и передайте его в качестве параметра макросу VBA:

    IMyMainFactory factory = // ... create factory
    ThisApplication.Run("RegisterFactory", factory, Type.Missing, ...);
    
  • В рабочей книге VSTO создайте макрос RegisterFactoryи сохраните экземпляр класса фабрики в глобальной переменной:

    Option Explicit
    Private objFactory As Object
    
    Public Sub RegisterFactory(Factory As Object)
        Set objFactory = Factory
    End Sub
    
  • Создайте приложение VSTO и преобразуйте книгу VSTO в надстройку xla.Вы можете сделать это, используя код VB, VBA или VBScript, например:

    Set w = Application.Workbooks.Open("MyVstoWorkbook.xls", ...)
    w.IsAddin = True
    w.SaveAs "MyVstoAddIn.xla", 18, ...
    

В результате вышеизложенного, каждый раз, когда вы загружаете надстройку MyVstoAddIn.xla, она создаетваш завод и сохранить его в глобальной переменной в модуле VBA.Вы можете получить к нему доступ из кода VBA (который также будет содержать ссылку на сгенерированную выше TypeLib), и вы уже в работе.

Существует ряд преимуществ по сравнению со стандартным COM-взаимодействием, не в последнюю очередь эточто ваша надстройка VSTO имеет свой собственный домен приложения и файл конфигурации приложения, поэтому вы не конфликтуете с другим управляемым кодом.

...