I am unable to get my C# Dll to work with my Excel Macro enable spreadsheet to call the function in the Dll. I am able to call the function if it has no parameters, able to return the expected value. But I cannot get the Dll call from VBA to be successful when I add input parameters to the function.
The sum total of my C# module is as follows:
using System;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;
namespace RSExternalInterface
{
public class RSExternalInterface
{
[DllExport("add", CallingConvention = CallingConvention.Cdecl)]
public static int TestExport(int left, int right)
{
return left + right;
}
}
}
My VBA Code is as follows.
Module Declaration:
Declare Function add Lib "C:\Program Files\RS\RSExternalInterface.dll" (ByVal Left As Integer, ByVal Right as Integer) As Integer
Call the function above, inside the click event sub of a button, is as follows:
result = add (5, 7)
The moment the above line is executed, Error 49 comes up.
I have tried the following:
Removed the "ByVal" in the VBA function declaration
Removed and added the decoration in the C# function, after [DllExports ...]
Ensured that I target the correct CPU platform
Removed the parameters in the declaration and calling to ensure that the DLL function is accessible.
What am I doing wrong?
I am surprised that "Calling Convention" from question title and CallingConvention from C# code didn't ring any bell (although I don't rule out a misleading error message on MS side).
Calling convention is like a protocol when executing subroutines, between:
callee - the routine (function, procedure (or function that doesn't return anything, or returns void))
caller - code (a subroutine as well - could be main) that calls/executes the callee
and determines:
Who handles (push / pop) the function arguments on the stack (callee / caller) when executing it
Arguments order on the stack (right -> left or left -> right)
Because of #1., it's important that the 2 parties (callee / caller) to be in synch regarding calling convention, otherwise they will setp on each other's toes, and the stack will eventually get corrupted. "Bad DLL Calling Convention" means exactly this thing: the 2 are not in synch.
You get correct behavior for function without arguments, because there's nothing to push / pop to/from the stack - this doesn't mean that there isn't any error; the error is there but it's not encountered
More details: [MSDN]: Argument Passing and Naming Conventions. Note differences between __stdcall and __cdecl, and also note that they apply to 32bit (x86) architecture. AFAIK (also backed up by the error), Excel executable (as whole MSOffice) is 32bit
According to [MSDN]: DllImportAttribute.CallingConvention Field (which I assume also applies to DllExport):
You set this field to one of the CallingConvention enumeration members. The default value for the CallingConvention field is Winapi, which in turn defaults to StdCall convention.
I took your code, and played a little bit with it.
code.cs:
using System;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;
namespace RSExternalInterface
{
public class RSExternalInterface
{
[DllExport("add")]
public static int TestExport(int left, int right)
{
int ret = left + right;
String s = String.Format("C# Func - add: {0} + {1} = {2}", left, right, ret);
Console.WriteLine(s);
return ret;
}
[DllExport("dbl", CallingConvention = CallingConvention.Winapi)]
public static int TestExport1(int value)
{
int ret = value * 2;
String s = String.Format("C# Func - dbl: {0} * 2 = {1}", value, ret);
Console.WriteLine(s);
return ret;
}
[DllExport("none", CallingConvention = CallingConvention.StdCall)]
public static void TestExport2(int value)
{
String s = String.Format("C# Func - none: {0}", value);
Console.WriteLine(s);
}
}
}
main.vb:
Module main
Declare Function add Lib "CsDll.dll" (ByVal Left As Integer, ByVal Right As Integer) As Integer
Declare Function dbl Lib "CsDll.dll" (ByVal Value As Integer) As Integer
Declare Sub none Lib "CsDll.dll" (ByVal Value As Integer)
Sub Main()
Console.WriteLine("64 bit OS: {0}{1}64 bit process: {2}{1}", Environment.Is64BitOperatingSystem, Environment.NewLine, Environment.Is64BitProcess)
Dim i As Integer
none(-7659)
i = dbl(78)
i = add(22, -13)
End Sub
End Module
Notes:
I wasn't able to build it and run it in Excel, so I tried the 2nd best thing: do it from VB. Note that I'm experienced in neither C# nor VB
At the beginning (as I stated in a comment), I wasn't able to export anything from the C# .dll. Looking at UnmanagedExports.nuspec (part of the nupkg):
You have to set your platform target to either x86, ia64 or x64. AnyCPU assemblies cannot export functions.
Changed the Platform from Any CPU (VStudio default) to x64 (by mistake), and everything ran fine (in spite of CallingConvention.Cdecl), only after setting it to x86 I was able to reproduce the problem
Anyway, the (C#) code contains 3 ways of (not) specifying a calling convention, that will work from VB
Output (VStudio 2015 (Community)):
64 bit OS: True
64 bit process: False
C# Func - none: -7659
C# Func - dbl: 78 * 2 = 156
C# Func - add: 22 + -13 = 9
Here's an (ancient) URL that also mentions your error: [MSDN]: Bad DLL calling convention (Error 49)
Related
I have a C lib and want to call function in this library from C# application. I tried creating a C++/CLI wrapper on the C lib by adding the C lib file as linker input and adding the source files as additional dependencies.
Is there any better way to achieve this as am not sure how to add C output to c# application.
My C Code -
__declspec(dllexport) unsigned long ConnectSession(unsigned long handle,
unsigned char * publicKey,
unsigned char publicKeyLen);
My CPP Wrapper -
long MyClass::ConnectSessionWrapper(unsigned long handle,
unsigned char * publicKey,
unsigned char publicKeyLen)
{
return ConnectSession(handle, publicKey, publicKeyLen);
}
The example will be, for Linux:
1) Create a C file, libtest.c with this content:
#include <stdio.h>
void print(const char *message)
{
printf("%s\\n", message);
}
That’s a simple pseudo-wrapper for printf. But represents any C function in the library you want to call. If you have a C++ function don’t forget to put extern C to avoid mangling the name.
2) create the C# file
using System;
using System.Runtime.InteropServices;
public class Tester
{
[DllImport("libtest.so", EntryPoint="print")]
static extern void print(string message);
public static void Main(string[] args)
{
print("Hello World C# => C++");
}
}
3) Unless you have the library libtest.so in a standard library path like “/usr/lib”, you are likely to see a System.DllNotFoundException, to fix this you can move your libtest.so to /usr/lib, or better yet, just add your CWD to the library path: export LD_LIBRARY_PATH=pwd
credits from here
EDIT
For Windows, it's not much different.
Taking an example from here, you only have yo enclose in your *.cpp file your method with extern "C"
Something like
extern "C"
{
//Note: must use __declspec(dllexport) to make (export) methods as 'public'
__declspec(dllexport) void DoSomethingInC(unsigned short int ExampleParam, unsigned char AnotherExampleParam)
{
printf("You called method DoSomethingInC(), You passed in %d and %c\n\r", ExampleParam, AnotherExampleParam);
}
}//End 'extern "C"' to prevent name mangling
then, compile, and in your C# file do
[DllImport("C_DLL_with_Csharp.dll", EntryPoint="DoSomethingInC")]
public static extern void DoSomethingInC(ushort ExampleParam, char AnotherExampleParam);
and then just use it:
using System;
using System.Runtime.InteropServices;
public class Tester
{
[DllImport("C_DLL_with_Csharp.dll", EntryPoint="DoSomethingInC")]
public static extern void DoSomethingInC(ushort ExampleParam, char AnotherExampleParam);
public static void Main(string[] args)
{
ushort var1 = 2;
char var2 = '';
DoSomethingInC(var1, var2);
}
}
UPDATE - Feb 22 2019: Since this answer has been getting quite a few upvotes, I decided to update it with a better way of calling the C method. Previously I had suggested using unsafe code, but the safe and correct way is to use MarshalAs attribute for converting a .NET string to a char*. Also, in VS2017 there is no Win32 project anymore, you'll probably have to create a Visual C++ dll or empty project and modify that. Thank you!
You can directly call C functions from C# by using P/Invoke.
Here's a short how-to on creating a C# lbrary that wraps around a C dll.
Create a new C# Library project (I'll call it "Wrapper")
Add a Win32 project to the solution, set application type to: DLL (I'll call it "CLibrary")
You can remove all the other cpp/h files since we won't need them
Rename the CLibrary.cpp file to CLibrary.c
Add a CLibrary.h header file
Now we need to configure the CLibrary project, right-click it and go to properties, and select Configuration: "All Configurations"
In Configuration Properties > C/C++ > Precompiled headers, set Precompiled Headers to: "Not using Precompiled Headers"
In the same C/C++ branch, go to Advanced, change Compile As to: "Compile as C code (/TC)"
Now in the Linker branch, go to General, and change Output File to: "$(SolutionDir)Wrapper\$(ProjectName).dll", this will copy the built C DLL to the C# project root.
CLibrary.h
__declspec(dllexport) unsigned long ConnectSession(unsigned long handle,
unsigned char * publicKey,
unsigned char publicKeyLen);
CLibrary.c
#include "CLibrary.h"
unsigned long ConnectSession(unsigned long handle,
unsigned char * publicKey,
unsigned char publicKeyLen)
{
return 42;
}
Right-click CLibrary project, build it, so we get the DLL in the C# project directory
Right-click C# Wrapper project, add existing item, add CLibrary.dll
Click on CLibrary.dll, go to the properties pane, set "Copy to output Directory" to "Copy Always"
It's a good idea to make the Wrapper project dependent on CLibrary so CLibrary gets built first, you can do that by right-clicking the Wrapper project, going to "Project Dependencies" and checking "CLibrary".
Now for the actual wrapper code:
ConnectSessionWrapper.cs
using System.Runtime.InteropServices;
namespace Wrapper
{
public class ConnectSessionWrapper
{
[DllImport("CLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
static extern uint ConnectSession(uint handle,
[MarshalAs(UnmanagedType.LPStr)] string publicKey,
char publicKeyLen);
public uint GetConnectSession(uint handle,
string publicKey,
char publicKeyLen)
{
return ConnectSession(handle, publicKey, publicKeyLen);
}
}
}
Now just call GetConnectSession, and it should return 42.
Result:
Okay well, Open VS 2010, Goto File -> New -> Project -> Visual C++ -> Win32 -> Win32 Project and give it a name (HelloWorldDll in my case), Then in the window that follows under Application Type choose 'DLL' and under Additonal Options choose 'Empty Project'.
Now goto your Solution Explorer tab usually right hand side of VS window, right click Source Files -> Add Item -> C++ file (.cpp) and give it a name (HelloWorld in my case)
Then in the new class paste this code:
#include <stdio.h>
extern "C"
{
__declspec(dllexport) void DisplayHelloFromDLL()
{
printf ("Hello from DLL !\n");
}
}
Now Build the project, after navigate to your projects DEBUG folder and there you should find: HelloWorldDll.dll.
Now, lets create our C# app which will access the dll, Goto File -> New -> Project -> Visual C# -> Console Application and give it a name (CallDllCSharp), now copy and paste this code to your main:
using System;
using System.Runtime.InteropServices;
...
static void Main(string[] args)
{
Console.WriteLine("This is C# program");
DisplayHelloFromDLL();
Console.ReadKey();
}
and build the program, now that we have both our apps built lets use them, get your *.dll and your .exe (bin/debug/.exe) in the same directory, and execute the application output should be
This is C# program
Hello from DLL !
Hope that clears some of your issues up.
References:
How to create a DLL library in C and then use it with C#
NOTE : BELOW CODE IS FOR MULTIPLE METHODS FROM DLL.
[DllImport("MyLibc.so")] public static extern bool MyLib_GetName();
[DllImport("MyLibc.so")] public static extern bool MyLib_SetName(string name);
[DllImport("MyLibc.so")] public static extern bool MyLib_DisplayName(string name);
public static void Main(string[] args)
{
string name = MyLib_GetName();
MyLib_SetName(name);
MyLib_DisplayName(name);
}
The P/Invoke method has been described extensively and repeatedly, ok so far.
What I'm missing here is, that the C++/CLI method has a big advantage: Calling safety.
In contrast to P/Invoke, where the call of the C funtion is like shooting blind into the sky (if this comparison is allowed), nobody will check the function arguments when calling the C function.
Using C++/CLI in this case means normally, you include a headerfile with the functions prototypes you want to use. If you are calling the C function with wrong/to much /to few arguments, the compiler will tell you.
I don't think you can say in general which is the better method, honestly I don't like either of them.
I found a way to call .NET 2 code directly from VBA code:
Dim clr As mscoree.CorRuntimeHost
Set clr = New mscoree.CorRuntimeHost
clr.Start
Dim domain As mscorlib.AppDomain
clr.GetDefaultDomain domain
Dim myInstanceOfDotNetClass As Object
Set myInstanceOfDotNetClass = domain.CreateInstanceFrom("SomeDotNetAssembly.dll", "Namespace.Typename").Unwrap
Call myInstanceOfDotNetClass.ExecuteSomeDotNetMethod
I added references to mscoree.tlb and mscorlib.tlb to Excel VBA using Tools -> References.
This works for .NET CLR 2 assemblies, up to .NET framework version 3.5.
I need to make it work with .NET 4.
I understood that .NET CLR4 introduced another, version agnostic, way of creating an instance of the runtime and I have found a code example written in C++:
http://dev.widemeadows.de/2014/02/04/hosting-the-net-4-runtime-in-a-native-process/
My Excel VBA skills are not enough to translate those few lines of code.
Here is a canonical answer on the 3 main methods to call .Net from Excel (or VBA).
All three ways work in .Net 4.0.
1. XLLs
The 3rd party vendor Add-In Express offer XLL functionality, however its free and easy to use Excel-DNA the author is here https://stackoverflow.com/users/44264
Here is an extract from the Excel-DNA page: https://excel-dna.net/
Introduction
Excel-DNA is an independent project to integrate .NET into Excel. With Excel-DNA you can make native (.xll) add-ins for Excel using C#, Visual Basic.NET or F#, providing high-performance user-defined functions (UDFs), custom ribbon interfaces and more. Your entire add-in can be packed into a single .xll file requiring no installation or registration.
Getting Started
If you are using a version of Visual Studio that supports the NuGet Package Manager (including Visual Studio 2012 Express for Windows Desktop), the easiest way to make an Excel-DNA add-in is to:
Create a new Class Library project in Visual Basic, C# or F#.
Use the Manage NuGet Packages dialog or the Package Manager Console to install the Excel-DNA package:
PM> Install-Package Excel-DNA
Add your code (C#, Visual Basic.NET or F#):
using ExcelDna.Integration;
public static class MyFunctions
{
[ExcelFunction(Description = "My first .NET function")]
public static string SayHello(string name)
{
return "Hello " + name;
}
}
Compile, load and use your function in Excel:
=SayHello("World!")
2. Automation AddIns
This article by Eric Carter shows how to do it, the article is missing heaps of images so I am copy / pasting the entire article and have recreated the images for preservation.
REF: https://blogs.msdn.microsoft.com/eric_carter/2004/12/01/writing-user-defined-functions-for-excel-in-net/
Excel enables the creation of user defined functions that can be used in Excel formulas. A developer must create a special kind of DLL called an XLL. Excel also allows you to write custom functions in VBA that can be used in Excel formulas. Unfortunately, Excel does not support or recommend writing an XLL that uses managed code. If you are willing to take your chances that your XLL might not run in current or future versions of Excel, there are solutions available that enable this scenario—search the web for “managed XLL”.
Fortunately, there is an easier way to create a user defined function that doesn’t require you to create an XLL dll. Excel XP, Excel 2003, and Excel 2007 support something called an Automation Add-in. An Automation Add-in can be created quite simply in C# or VB.NET. I’m going to show you an example in C#.
First, launch Visual Studio and create a new C# class library project called AutomationAddin for this example.
Then, in your Class1.cs file, enter the code shown below. Replace the GUID with your own GUID that you create by using Generate GUID in the Tools menu of Visual Studio.
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace AutomationAddin
{
// Replace the Guid below with your own guid that
// you generate using Create GUID from the Tools menu
[Guid("A33BF1F2-483F-48F9-8A2D-4DA68C53C13B")]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class MyFunctions
{
public MyFunctions()
{
}
public double MultiplyNTimes(double number1, double number2, double timesToMultiply)
{
double result = number1;
for (double i = 0; i < timesToMultiply; i++)
{
result = result * number2;
}
return result;
}
[ComRegisterFunctionAttribute]
public static void RegisterFunction(Type type)
{
Registry.ClassesRoot.CreateSubKey(GetSubKeyName(type, "Programmable"));
RegistryKey key = Registry.ClassesRoot.OpenSubKey(GetSubKeyName(type, "InprocServer32"), true);
key.SetValue("", System.Environment.SystemDirectory + #"\mscoree.dll",RegistryValueKind.String);
}
[ComUnregisterFunctionAttribute]
public static void UnregisterFunction(Type type)
{
Registry.ClassesRoot.DeleteSubKey(GetSubKeyName(type, "Programmable"), false);
}
private static string GetSubKeyName(Type type, string subKeyName)
{
System.Text.StringBuilder s = new System.Text.StringBuilder();
s.Append(#"CLSID\{");
s.Append(type.GUID.ToString().ToUpper());
s.Append(#"}\");
s.Append(subKeyName);
return s.ToString();
}
}
}
With this code written, show the properties for the project by double clicking on the properties node under the project in Solution Explorer. Click on the Build tab and check the check box that says “Register for COM Interop”. At this point you have an extra step if you are running on Windows Vista or higher. Visual Studio has to be run with administrator privileges to register for COM interop. Save your project and exit Visual Studio. Then find Visual Studio in the Start menu and right click on it and choose “Run as Administrator”. Reopen your project in Visual Studio. Then choose “Build” to build the add-in.
Now launch Excel and get to the Automation servers dialog by following these steps:
Launch Excel and click the Microsoft Office button in the top left corner of the window.
Choose Excel Options.
Click the Add-Ins tab in the Excel Options dialog.
Choose Excel Add-Ins from the combo box labeled Manage. Then click the Go button.
Click the Automation button in the Add-Ins dialog.
You can find the class you created by looking for AutomationAddin.MyFunctions in the list of Automation add-ins:
Now, let’s try to use the function MultiplyNTimes inside Excel. First create a simple spreadsheet that has a number, a second number to multiple the first by, and a third number for how many times you want to multiply the first number by the second number. An example spreadsheet is shown here:
Click on an empty cell in the workbook below the numbers and then click on the Insert Function button in the formula bar. From the dialog of available formulas, drop down the “Or select a category” drop down box and choose “AutomationAddin.MyFunctions.
Then click on the MultiplyNTimes function as shown here:
When you press the OK button, Excel pops up a dialog to help you grab function arguments from the spreadsheet as shown here:
Finally, click OK and see your final spreadsheet as shown here with your custom formula in cell C3.
3. Calling .Net from Excel VBA
Using the code from the Automation.AddIn project we can easily call the MultiplyNTimes function from Excel VBA.
First Add a reference to the DLL from Excel, to do this you will need to be in the VB Editor. Press Alt + F11, then click Tools menu and References:
Select the AutomationAddIn DLL:
Add VBA code to call the .Net DLL:
Sub Test()
Dim dotNetClass As AutomationAddIn.MyFunctions
Set dotNetClass = New AutomationAddIn.MyFunctions
Dim dbl As Double
dbl = dotNetClass.MultiplyNTimes(3, 2, 5)
End Sub
And hey presto!
Please note if you're working with Classes in C# you will need to mark them with ClassInterface, with an Interface marked with ComVisible = true: Use CLR classes from COM addin in Excel VBA?
Finally there are some excellent MSDN articles about Excel and .Net by "Andrew Whitechapel" - google them
The default policy is preventing the CLR 4 from excuting the legacy code from the CLR 2 :
Set clr = New mscoree.CorRuntimeHost
To enable the legacy execution, you can either create the file excel.exe.config in the folder where excel.exe is located:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0"/>
</startup>
</configuration>
Or you can call the native function CorBindToRuntimeEx instead of New mscoree.CorRuntimeHost :
Private Declare PtrSafe Function CorBindToRuntimeEx Lib "mscoree" ( _
ByVal pwszVersion As LongPtr, _
ByVal pwszBuildFlavor As LongPtr, _
ByVal startupFlags As Long, _
ByRef rclsid As Long, _
ByRef riid As Long, _
ByRef ppvObject As mscoree.CorRuntimeHost) As Long
Private Declare PtrSafe Function VariantCopy Lib "oleaut32" (dest, src) As Long
''
' Creates a .Net object with the CLR 4 without registration. '
''
Function CreateInstance(assembly As String, typeName As String) As Variant
Const CLR$ = "v4.0.30319"
Static domain As mscorlib.AppDomain
If domain Is Nothing Then
Dim host As mscoree.CorRuntimeHost, hr&, T&(0 To 7)
T(0) = &HCB2F6723: T(1) = &H11D2AB3A: T(2) = &HC000409C: T(3) = &H3E0AA34F
T(4) = &HCB2F6722: T(5) = &H11D2AB3A: T(6) = &HC000409C: T(7) = &H3E0AA34F
hr = CorBindToRuntimeEx(StrPtr(CLR), 0, 3, T(0), T(4), host)
If hr And -2 Then err.Raise hr
host.Start
host.GetDefaultDomain domain
End If
VariantCopy CreateInstance, domain.CreateInstanceFrom(assembly, typeName).Unwrap
End Function
Here's your solution, tested for .NET 2.0 and .NET 4.0, 32 bit and 64 bit, courtesy of Soraco Technologies.
The solution proposed below uses late binding and does not require registration of the .NET assemblies.
Declarations
Add the following declarations to your project:
#If VBA7 Then
Private Declare PtrSafe Function GetShortPathName Lib “Kernel32.dll” Alias “GetShortPathNameW” (ByVal LongPath As LongPtr, ByVal ShortPath As LongPtr, ByVal Size As Long) As Long
Private Declare PtrSafe Function SetDllDirectory Lib “Kernel32.dll” Alias “SetDllDirectoryW” (ByVal Path As LongPtr) As Long
Private Declare PtrSafe Sub LoadClr_x64 Lib “QlmCLRHost_x64.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
Private Declare PtrSafe Sub LoadClr_x86 Lib “QlmCLRHost_x86.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
#Else
Private Declare Function GetShortPathName Lib “Kernel32.dll” Alias “GetShortPathNameW” (ByVal LongPath As Long, ByVal ShortPath As Long, ByVal Size As Long) As Long
Private Declare Function SetDllDirectory Lib “Kernel32.dll” Alias “SetDllDirectoryW” (ByVal Path As Long) As Long
Private Declare Sub LoadClr_x64 Lib “QlmCLRHost_x64.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
Private Declare Sub LoadClr_x86 Lib “QlmCLRHost_x86.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
#End If ‘ WinAPI Declarations
' Declare variables
Dim m_myobject As Object
Dim m_homeDir As String
Initialization
You must initialize the m_homeDir variable to the path where the .NET assemblies are located.
For example, if you install the .NET assemblies in the same folder as the Excel or MS-Access files, you should initialize m_homeDir to:
Excel: m_homeDir = ThisWorkbook.Path
Access: m_homeDir = CurrentProject.Path
.NET Object Creation
Add the following code to your project.
Private Function GetMyObject(dllPath As String, dllClass As String) As Object
Dim LongPath As String
Dim ShortPath As String
LongPath = “\\?\” & m_homeDir
ShortPath = String$(260, vbNull)
PathLength = GetShortPathName(StrPtr(LongPath), StrPtr(ShortPath), 260)
ShortPath = Mid$(ShortPath, 5, CLng(PathLength – 4))
Call SetDllDirectory(StrPtr(ShortPath))
Dim clr As mscoree.CorRuntimeHost
If Is64BitApp() Then
Call LoadClr_x64(“v4.0”, False, clr)
Else
Call LoadClr_x86(“v4.0”, False, clr)
End If
Call clr.Start
Dim domain As mscorlib.AppDomain
Call clr.GetDefaultDomain(domain)
Dim myInstanceOfDotNetClass As Object
Dim handle As mscorlib.ObjectHandle
Set handle = domain.CreateInstanceFrom(dllPath, dllClass)
Dim clrObject As Object
Set GetMyObject = handle.Unwrap
Call clr.Stop
End Function
Private Function Is64BitApp() As Boolean
#If Win64 Then
Is64BitApp = True
#End If
End Function
Instantiate the .NET object
Now you are ready to instantiate your .NET object and start using it. Add the following code to your application:
m_homeDir = ThisWorkbook.Path
m_myobject = GetMyObject(m_homeDir & “\yourdotnet.dll”, “namespace.class”)
The first argument is the full path to the .NET DLL.
The second argument is the fully qualified name of the requested type, including the namespace but not the assembly, as returned by the Type.FullName property.
Required DLLs
The solution requires deployment of 2 DLLs that are responsible for hosting the .NET CLR. The DLLs are expected to be deployed in the same folder as your Excel or MS-Access file.
The DLLs can be downloaded from Soraco’s web site: https://soraco.co/products/qlm/QLMCLRHost.zip
Licensing LGPL-2.1
We hereby grant you the right to use our DLLs as long as your application does not compete directly or indirectly with Quick License Manager. You can use these DLLs in your commercial or non-commercial applications.
I'm not sure if this was just a coincidence or because I posted related question. SO showed me your question and I think I could also contribute something.
When working with VBA and DLL, most solutions that I've seen so far is telling me to register the DLL and make it com/gac visible. If you are doing this in your PC that's absolutely fine but if you are distributing your VBA application, you don't really want to install DLLs in their system. You might not have permission or you don't really want to go through install/uninstall process or messing with referencing issues.
However you can load dlls dynamically using some windows APIs.
DLL
Now the question is how to access .NET dll from vba? if your clients have mixed os architecture x86 x64 you need to handle this accordingly. Lets assume we are working on 32bit office/Excel.
If you create a .NET dll and would like to access it from VBA it will throw an error message similar to "Can't find the dll entry point". thankfully Robert Giesecke has created an abstract wrapper which will allow you to create simple DLL consumable via VBA.
A template can be found here.
All you have to do
Create a new class project in visual studio
Set the project platform either x86 for 32bit and otherwise
Create your methods within a main class.
create another class which will return your main class as object (is returning to vba)
(follow the template from his website)
Lets assume you have followed his template and created a test method as following.
[ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual)]
public class YOUR_MAIN_CLASS
{
[return: MarshalAs(UnmanagedType.BStr)]
public string FN_RETURN_TEXT(string iMsg)
{
return "You have sent me: " + iMsg + "...";
}
}
and your unmanagedexport class:
static class UnmanagedExports
{
[DllExport]
[return: MarshalAs(UnmanagedType.IDispatch)]
static object YOUR_DLL_OBJECT()
{
return new YOUR_MAIN_CLASS();
}
}
Preparing to access the dll from vba side
Add the DLL to your root folder:
#If VBA7 Then
Public Declare PtrSafe Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As LongPtr
Public Declare PtrSafe Function YOUR_DLL_OBJECT Lib "YOUR_DLL.dll" () As Object
#Else
Public Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal strFilePath As String) As Long
Public Declare Function YOUR_DLL_OBJECT Lib "YOUR_DLL.dll" () As Object
#End If
Now It's all about loading the dll and creating & accessing objects it in vba.
that would be:
LoadLibrary (FN_APP_GET_BASE_PATH & "YOUR_DLL.dll")
dim mObj as object
set mObj = YOUR_DLL_OBJECT()
debug.print mObj.FN_RETURN_TEXT("Testing ..")
the output should be
"You have sent me: Testing ....."
Advantages
I personally don't like installing and referencing dlls. By following above template, you don't need to reference anything, you don't need to install anything just load and work with your the DLL with full freedom.
NOTE: I assume the dll/.net code is yours and you can compile it again with above templates to.
I had success with above template and created a .NET non-blocking notifications for vba you can have a look here: non-blocking "toast" like notifications for Microsoft Access (VBA)
I need to add new functionality to vba in access. I already tried a lot of tutorials:
http://www.office-loesung.de/ftopic268792_0_0_asc.php (german) and
http://www.codeproject.com/Articles/555660/Extend-your-VBA-code-with-Csharp-VB-NET-or-Cpluspl (cant post more cause of rep)
but none of them works for me. The customer has office 2013 and i am using .net 4.5.
We want to provide the customer a rest api which is already written in c#/.net and which needs some encryption dlls of the .net world.
I tried to create an dll with "Interop COM" switch on and could add a reference of the dll to my VBA test Sheet BUT neither objects nor static test functions have been working. There even is no intelisense in VBA. A restart of the program did not solve the problem either. (though somehting went wrong before)
The error message i retrieve is "Runtime Error 429 - ActiveX Component Can't Create Object" (for my own code as well as for the code from codeplex (with all modules, C#, VBA, C++/Cli))
Is there any other way to go, adding new functionality to VBA / extending it with own functions?
Okay so I finally found a way myself.
In this post calling managed c# functions from unmanaged c++ there is the NuGet Package Unmanaged Exports linked.
So I created a new C# DLL Project, inserted for testing purpose following code:
using RGiesecke.DllExport;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ClassLibrary1
{
internal static class Class1
{
[DllExport("square", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
static Double Square(Double dateValue)
{
return dateValue * dateValue;
}
[DllExport("test", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
static String Test()
{
return " ok";
}
[DllExport("mytest2", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
static void Test4(String doSomething)
{
// Manipulate (and obisously create new) String
doSomething += " ok";
}
}
}
Compiled this with x64 switch on, and created an VBA Sheet in excel with:
Option Explicit
Private Declare PtrSafe Function square Lib "C:\Users\<Name>\documents\visual studio 2013\Projects\ClassLibrary1\ClassLibrary1\bin\x64\Release\ClassLibrary1.dll" (ByVal dateValue As Double) As Double
Private Declare PtrSafe Function test Lib "C:\Users\<Name>\documents\visual studio 2013\Projects\ClassLibrary1\ClassLibrary1\bin\x64\Release\ClassLibrary1.dll" () As String
Private Declare PtrSafe Function mytest2 Lib "C:\Users\<Name>\documents\visual studio 2013\Projects\ClassLibrary1\ClassLibrary1\bin\x64\Release\ClassLibrary1.dll" (ByRef dateValue As String)
Private Sub CommandButton1_Click()
Dim zahl As Double
zahl = CInt(TextBox1.text)
zahl = square(zahl)
Label1.Caption = CStr(zahl)
Dim text As String
text = "This is"
mytest2 (text)
MsgBox text
End Sub
The only error I still get is when calling the test function. Returning an string seems to break excel. (Program instantly quits) But I think that should be solved in another question.
I want to use a dll that made by Delphi. It has this function :
function CryptStr(str, Key : AnsiString; DecryptStr : boolean) : AnsiString; stdcall;
I copied the Dll in /bin/debug and in application root. my code is :
[DllImport("Crypt2.dll", EntryPoint = "CryptStr", CallingConvention = CallingConvention.StdCall)]
static extern string CryptStr( string str, string Key, bool DecryptStr);
public string g = "";
private void Form1_Load(object sender, EventArgs e)
{
g=CryptStr("999", "999999", true);
MessageBox.Show(g);
}
I have some problem :
1. even I delete Dll from those path application doesn't throw not found exception
2. when application run in g=CryptStr("999", "999999", true); it finishes execution and show the form without running Messagebox line.
I tried to use Marshal but above errors remain.
You cannot expect to call that function from a programming environment other than Delphi. That's because it uses Delphi native strings which are not valid for interop. Even if you call from Delphi you need to use the same version of Delphi as was used to compile the DLL, and the ShareMem unit so that the memory manager can be shared. That function is not even well designed for interop between two Delphi modules.
You need to change the DLL function's signature. For example you could use:
procedure CryptStr(
str: PAnsiChar;
Key: PAnsiChar;
DecryptStr: boolean;
output: PAnsiChar;
); stdcall;
In C# you would declare this like so:
[DllImport("Crypt2.dll")]
static extern void CryptStr(
string str,
string Key,
bool DecryptStr,
StringBuilder output
);
This change requires the caller to allocate the buffer that is passed to the function. If you want to find examples of doing that, search for examples calling the Win32 API GetWindowText.
If you were using UTF-16 text instead of 8 bit ANSI, you could use COM BSTR which is allocated on the shared COM heap, but I suspect that option is not available to you.
As for your program not showing any errors, I refer you to these posts:
http://blog.paulbetts.org/index.php/2010/07/20/the-case-of-the-disappearing-onload-exception-user-mode-callback-exceptions-in-x64/
http://blog.adamjcooper.com/2011/05/why-is-my-exception-being-swallowed-in.html
Silent failures in C#, seemingly unhandled exceptions that does not crash the program
I have a particular function in an Excel addin(xll).
The addin is proprietary and we do not have access to the source code. However we need to call some functions contained within the addin and we would like to call it from a C# program.
Currently, I was thinking of writing a C++ interface calling the Excel function with xlopers, then calling this C++ interface from C#.
Does anybody who has prior experience of this kind of issues know what would be the best solution for that ?
Anthony
You need to create a fake xlcall32.dll, put it in the same directory as your XLL (do not put excel's own xlcall32.dll in the PATH). Here is some code:
# include <windows.h>
typedef void* LPXLOPER;
extern "C" void __declspec(dllexport) XLCallVer ( ) {}
extern "C" int __declspec(dllexport) Excel4 (int xlfn, LPXLOPER operRes, int count,... ) { return 0; }
extern "C" int __declspec(dllexport) Excel4v(int xlfn, LPXLOPER operRes, int count, LPXLOPER far opers[]) {return 0;}
Now suppose I have an XLL called xll-dll.xll with a function called (use "depends.exe" to find out the names of the exported functions) xlAdd that well adds two doubles:
extern "C" __declspec(dllexport) XLOPER * __cdecl xlAdd(XLOPER* pA, XLOPER* pB);
The following code calls it:
# include <windows.h>
# include <iostream>
// your own header that defines XLOPERs
# include <parser/xll/xloper.hpp>
// pointer to function taking 2 XLOPERS
typedef XLOPER * (__cdecl *xl2args) (XLOPER* , XLOPER* ) ;
void test(){
/// get the XLL address
HINSTANCE h = LoadLibrary("xll-dll.xll");
if (h != NULL){
xl2args myfunc;
/// get my xll-dll.xll function address
myfunc = (xl2args) GetProcAddress(h, "xlAdd");
if (!myfunc) { // handle the error
FreeLibrary(h); }
else { /// build some XLOPERS, call the remote function
XLOPER a,b, *c;
a.xltype = 1; a.val.num = 1. ;
b.xltype = 1; b.val.num = 2. ;
c = (*myfunc)(&a,&b);
std::cout << " call of xll " << c->val.num << std::endl; }
FreeLibrary(h); }
}
int main()
{test();}
My exe actually works (to my own surprise), and output 3 as expected. You must have some knowledge of what your XLL actually expects for parameters. If it allocates some memory, you must check if the #define xlbitDLLFree 0x4000
is set on your XLOPER c->type, and call back "xlAutoFree".
You might like to try XLL Plus http://www.planatechsolutions.com/xllplus/default.htm. It's a bit pricey, but the
XLL Wrapper Libraries feature is exactly what you are looking for:
"Sometimes it is useful to be able to call your Excel add-in functions from other environments, such as command-line programs or interactive applications, written in C++, Java, C# or Visual Basic. The Xll Wrapper toolkit contains tools, a runtime library, samples and documentation to help with the development of COM modules and .NET assemblies that wrap Excel XLL add-ins"
An Excell add-in DLL might be written in C#. If so, then you can probably bypass Excel altogether. Might need a wrapper though.
check whether assembly is available in program files where you install the plguin,
reference that assembly directly to your c# project - check the object browser for your class, method or object
You should be able to use reflection to get access to your addin. try using The Reflector to see what's available.