从C#导出C函数并在VBA中使用

本文关键字:VBA 导出 函数 | 更新日期: 2023-09-27 17:57:47

我使用Robert Giesecke的解决方案http://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports将函数从托管代码导出到非托管代码。该解决方案运行得很好,但在office(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数据对象2.8库"。

我试着用C#使用导出的64位DLL,效果很好。我尝试使用导出的64位DLL作为C#的静态类,效果很好。我试图将导出的32位DLL与VB6一起使用,应用程序崩溃。我试图将导出的32位DLL与VBA(Excel)一起使用,应用程序崩溃。

我检查了32位DLL中导出的函数是否存在依赖性助行器。

为什么我不能在office(Excel)中使用32位DLL?


我当然有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会崩溃吗?一定有一个愚蠢的小错误!


谢谢你,罗伯特,因为每次你的代码都是完美的!如果我在VBA 中使用以下代码,我可以看到streamreader对象的内容

MsgBox instance.ReadtoEnd()

结果是:

"abcÉö~éêè@dkfjf->从VBA添加"

问题显然是ADODB.连接!!!!!

    [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崩溃。问题是在32位DLL中使用ADODB(C#和使用64位DLL没有问题)。你的解决方案没有问题!

从C#导出C函数并在VBA中使用

我无法回答您的问题,但您使用的第三方代码可能有未记录的限制或假设,这些限制或假设在Office中运行时不起作用。

但是,还有其他方法可以将托管API导出到Excel VBA。我使用的解决方案如下:

  • 在IDL中为要从.NET公开的API定义一组双COM接口。应该有一个主体Factory接口可以用作VBA的主入口点。Factory必须能够直接或间接实例化任何要公开的对象(下面描述的技术不允许VBA直接创建对象,因此需要使用Factory类来创建对象)。

  • 使用MIDL.EXE从IDL生成TypeLib,并使用TlbImp将其公开给.NET

  • 创建一个引用TlbImp生成的COM互操作程序集的.NET类库项目,并编写实现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加载项有自己的AppDomain和应用程序配置文件,因此不会与其他托管代码发生冲突。

正如我在邮件对话中问你的那样:你真的使用64位办公室吗
这是非常不可能的,这就是为什么我想提前检查。

即使你确实使用了64位Office,它仍然可以工作。有一件事你必须记住:当你从VBA调用DLL函数时,传递的字符串类型将是一个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

如果这对你有效,我们可以从那里到达你想去的任何地方。但重要的是要知道您有什么样的办公版本(CPU平台),以及这个简单的示例是否首先运行

我会再问你一些问题,因为你有点回避了一些问题(在这里和之前通过电子邮件),我真的不确定你到底尝试了什么:

  • 您没有将项目的CPU平台设置为x86
  • 如果不是,则选择x86子文件夹上的程序集
  • 您试图将UnmanagedType.LPStr用于字符串参数吗
    虽然我有很多托管/非托管Interop,但我以前从未处理过VBA,所以我不确定AnsiBStr

甚至不要尝试加载x86 DLL以外的任何东西,无法工作
帮自己一个忙,将项目的CPU平台更改为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的即时窗口中看到了什么?