I have a class library that has a class Calculator with the method Add to add the two numbers written in c#. I want to access the method in VBA macro from Word. I can access the method easily when I make assembly COM-visible and add the COM reference in VBA in the office.
But I have to load the .dll differently using Declare statement. I have tried the following way but I am unable to load the DLL and receive the error:
C# Class
namespace Calculator
{
public class BasicCalculator
{
public int Add(int number1, int number2)
{
var result = number1 + number2;
return result;
}
}
}
Visual Basic for Applications (VBA)
Option Explicit
#IF VBA7 ThEN
Public Declare PtrSafe Function Add Lib "C:\Calculator\Calcular.dll" (number1 As Integer, number2 As Integer) As Integer
#ELSE
Public Declare Function Add Lib "C:\Calculator\Calcular.dll" (number1 As Integer, number2 As Integer) As Integer
#END IF
Sub Calculate()
Dim n1 As Integer
Dim n2 As Integer
n1 = 20
n2 = 10
Dim result As Integer
result = Add(n1, n2)
Debug.Print result
End Sub
Is there a way to fix the issue?
I saw a similar solution where the DLL has been loaded using Declare statement (maybe c++ dll).
I saw a similar solution where the DLL has been loaded using Declare statement (maybe c++ dll).
Yes, in case of unmanaged assemblies it could be possible. But you have developed a .net based application which requires loading the .net runtime. That's why you need to expose it to the COM world by making the .net assembly COM-visible and adding the COM reference in VBA in the office.
Related
There is a legacy Visual Basic class library targetting .Net Standard 2.0 currently:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
</ItemGroup>
</Project>
There is also a class inside
Public Class Class1
Public Shared Function TryGetInteger(ByVal value As Object) As Integer
Dim out As Integer
If Integer.TryParse(value, out) Then
Return out
End If
Return Integer.MinValue
End Function
Public Shared Function BooleanToInt(ByVal bValue As Object) As Integer
If (bValue = DBNull.Value) Then
Return 0
ElseIf bValue = True Then
Return 1
Else
Return 2
End If
End Function
End Class
At this point everything works fine.
But changing <TargetFramework>netstandard2.0</TargetFramework> to 2.1 breaks the build with the following error in the Class1.TryGetInteger function:
Requested operation is not available because the runtime library function 'Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType' is not defined
How can this be fixed?
EDIT: Please disregard below idea:
The first idea comes from this closed github issue, that suggests adding <VBRuntime>Default</VBRuntime> to the project file, but this results in error:
could not find library 'Microsoft.VisualBasic.dll'
But it is already linked as a package... Any ideas?
EDIT: As mentioned is the comments, my take on the linked github issue is a bit misleading, so I guess I'm back to the beginning with the Requested operation is not available error.
Integer.TryParse() expects a String, but the input argument type is Object.
You can fix the issue like this:
Public Shared Function TryGetInteger(value As Object) As Integer
Dim out As Integer
If Integer.TryParse(value.ToString(), out) Then Return out
Return Nothing 'Same as Integer.MinValue in this context
End Function
The extra ToString() call is the same thing the old framework was doing for you implicitly. Now you're aware of it.
But really I'd do this:
Public Shared Function TryGetInteger(value As String) As Integer
Dim out As Integer
If Integer.TryParse(value, out) Then Return out
Return Nothing
End Function
Which will probably bring up a whole new set of compiler errors, but there will be value in going back and fixing them at those calling locations. Most of the errors can be fixed by adding .ToString() to the end of value when calling the function, and any that can't were almost certainly hiding bugs.
Similarly for the BooleanToInt() function, I'd use explicit types, and maybe overloading to get better results, like this:
Public Shared Function BooleanToInt(bValue As DBNull) As Integer
Return 0 'If this overload was selected, we know we want 0
End Function
Public Shared Function BooleanToInt(bValue As Boolean) As Integer
If bValue Then Return 1
Return 2
End Function
Public Shared Function BooleanToInt(bValue As Object) As Integer
Return 2
End Function
As a rule of thumb, anywhere you need to use the Object type directly is a magnet for bugs. It's a smell in the code and something to avoid.
The class methods are relying on the implicit type conversions implemented by the VB compiler when Option Strict is set to Off.
The safest way to rewrite the code would be to use a de-compiler and implement the conversion operations used by the compiler. That could get quite involved.
If there is some sanity in the code that calls the the class methods, then a simple rewrite like the following can be done to avoid the usage of compiler's helper conversion methods.
Public Shared Function TryGetInteger(ByVal value As Object) As Integer
Dim out As Integer
If Integer.TryParse(value.ToString, out) Then
Return out
End If
Return Integer.MinValue
End Function
Public Shared Function BooleanToInt(ByVal bValue As Object) As Integer
If (bValue Is DBNull.Value) Then
Return 0
Else
If TypeOf bValue Is Boolean Then
Return If(DirectCast(bValue, Boolean), 1, 2)
Else
Throw New ArgumentException("type of bValue must be either DbNull or Boolean")
End If
End If
End Function
I implemented a VB.NET DLL with a simple test function:
<ComVisible(True)>
Function TestString ( <MarshalAs(UnmanagedType.BStr)> xyz As String) As <MarshalAs(UnmanagedType.BStr)> String
Dim y As Integer
TestString = "Hello"
End Function
The function is dead simple. In VBA I declare the function appropriately:
Public Declare Function TestString Lib "myDLL.dll" (xyz As String) As String
From VBA I also obviously load the DLL. However the issue is that when I run the function like so:
Dim st as String
st = "Hello"
Debug.Print TestString(ByVal st)
I get a message saying bad DLL calling convention. On the other hand when I remove the <MarshalAs(UnmanagedType.BStr)> the function works BUT crashes shortly after printing "Hello".
What am I doing wrong?
Public Declare Function is the syntax used to call functions exported from a DLL, this is a different mechanism from COM.
To use COM add a reference to the DLL, create an instance of the class containing TestString then call across that instance: r = classInstance.TestString("Hello")
This is a cousin of the question "Can CodeDom create optional arguments when generating a c# method?"
And I tried the answer given there.
Still, when I attempt to compile, I get the following error:
error BC30455: Argument not specified for parameter 'optionalParam' of 'Public Function Bar(optionalParam As Integer) As Integer
I've distilled this down to the Visual Basic Compiler not supporting either OptionalAttribute, DefaultParameterValueAttribute, or both.
Here's the distilled code I'm compiling:
Imports System.Runtime.InteropServices
Namespace SSI.RuntimeGenerated.FunctionsNamespace
Public Class Functions
Public Function Foo() As Integer
return Bar()
End Function
Public Function Bar( _
<[Optional], DefaultParameterValue(1)> _
ByVal optionalParam As Integer) _
As Integer
return optionalParam
End Function
End Class
End Namespace
Compiling this with the following command:
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\vbc.exe" /t:library /out:foobar.dll foobar.vb /langversion:11
Produces the following output:
Microsoft (R) Visual Basic Compiler version 11.0.50709.17929
Copyright (c) Microsoft Corporation All rights reserved.
C:\<snip>\foobar.vb : error BC30455: Argument not specified for parameter
'optionalParam' of 'Public Function Bar(optionalParam As Integer) As Integer'.
return Bar()
~~~~~
If I change the method signature manually to be
Public Function Bar(Optional ByVal optionalParam As Integer) As Integer
then it compiles just fine.
So my questions are:
How to create optional arguments in Visual Basic generated through CodeDom?
Does vbc even support use of OptionalAttribute and/or DefaultParameterValueAttribute?
If vbc doesn't support those attributes, is there any possible way to create Optional arguments without resorting to using a CodeSnippetStatement?
If there's no other way, then is it possible to salvage other work I've done with CodeDom so I don't end up having to pretty much generate my methods by hand? Do I have to pretty much build the rest by hand inside a CodeSnippetTypeMember? Would even that work?
The OptionalAttribute is not supported in VB.NET. I cannot find any official documentation that specifically says so, but if you try to use it in a VB.NET project, it will have no effect. To create an optional parameter in VB.NET, you must use the Optional keyword, for instace:
Public Class Functions
Public Function Foo() As Integer
Return Bar()
End Function
Public Function Bar(Optional ByVal optionalParam As Integer = 1) As Integer
Return optionalParam
End Function
End Class
I have written my own function, which in C would be declared like this, using standard Win32 calling conventions:
int Thing( char * command, char * buffer, int * BufSize);
I have the following amount of Visual Basic code figured out, which should import the DLL file and call this function, wrapping it up to make it easy to call Thing("CommandHere",GetDataBackHere).
UPDATE: This code is now a working solution, as shown here:
Imports Microsoft.VisualBasic
Imports System.Runtime.InteropServices
Imports System
Imports System.Text
Namespace dllInvocationSpace
Public Class dllInvoker
' I tried attributes, but I could not make it build:
' <DllImport("thing1.dll", False, CallingConvention.Cdecl, CharSet.Ansi, "Thing", True, True, False, True)>
Declare Ansi Function Thing Lib "thing1.dll" (ByVal Command As String, ByRef Buffer As StringBuilder, ByRef BufferLength As Integer) As Integer
' This part contributed by helpful user:
Shared Function dllCall(ByVal Command As String, ByRef Results As String) As Integer
Dim Buffer As StringBuilder = New StringBuilder(65536)
Dim Length As Integer = Buffer.Capacity
Dim retCode As Integer = Thing(Command, Buffer, Length)
Results = Buffer.ToString()
'Debug.Assert(Results.Length = Length) ' This assertion is not true for me
Return retCode
End Function
End Class
End Namespace
I got the code to build by following the help received here, and then I had forgot the As Return Type (which got me a MarshalDirectiveException PInvokeRestriction). Then I had an assertion failure inside my DLL, which lead to an SEHException. Once fixed, this works BEAUTIFULLY. Thank you folks. There are newsgroups where people are saying this can not be done, that Visual Basic only loads managed DLL assemblies (which I guess is the normal thing most Visual Basic users are used to).
It depends on how you use the buffer argument in your C code. If you only pass a string from your VB.NET code to your C code then declaring it ByVal String is good enough. If however you let the C code return a string in the buffer then you have to declare it ByVal StringBuilder and initialize it properly before the call. For example:
Public Class dllInvoker
Declare Ansi Function Thing Lib "Thing1.dll" (ByVal Command As String, ByVal Buffer As StringBuilder, ByRef BufferLength As Integer) As Integer
Shared Function dllCall(ByVal Command As String, ByRef Results As String) As Integer
Dim Buffer As StringBuilder = New StringBuilder(65536)
Dim Length As Integer = Buffer.Capacity
Dim retCode As Integer = Thing(Command, Buffer, Length)
Results = Buffer.ToString()
Debug.Assert(Results.Length = Length)
Return retCode
End Function
End Class
Note the ambiguity in the returned Length value.
You cannot convert a StringBuilder instance to a string instance, instead, use the 'ToString' method to convert it back to the string type...here's the portion of the code in the dllCall function...
retCode = Thing(Command, Buffer, bufsz)
Results = Buffer.ToString();
I got a Utility module since VB.NET doesn't have static class like C# and Module is the static class in VB.NET. The reason that I use module is because I'm using the Extension method and it can only be use in Module.
I can't reference to this module but if I put my code in a class. I can reference to it without any problem. What could be the reason? I missed C#.
Edit: The module is inside a class library call Utility.
You need to mark the module as Public Module.
I can't reference to this module but if i put my code in a class. I can reference to it without any problem. Does anyone know why?
Because Modules in VB aren't classes and can't be used to instantiate objects. Rather, they're something similar to namespaces, with the difference that namespaces can't contain functions directly. So the reason for modules is to provide a way to group functions logically that don't belong to a class.
This makes a lot of sense when you consider that not everything logically belongs to a class. Consider System.Math. There is absolutely no reason to make that a class, other than a weird OOP purism.
By the way, you can't reference static classes in C# either, at least not if I understand correctly what you mean by “reference”. Perhaps you can clarify this.
.NET compilers can take any type of language syntax and turn it into a .NET equivalent. Sometimes there is a one for one correspondence other times there isn't.
By using the .NET Reflector you can see what the compiler is really doing.
In VB.NET the module exists because of the heritage inherited from Visual BASIC and partly from Microsoft BASIC.
The VB.NET compiler will take this
Public Module CoreModule
Dim R As New System.Random(CInt(Microsoft.VisualBasic.Timer))
Public Function D(ByVal Roll As Integer) As Integer
Return R.Next(0, Roll) + 1
End Function
Public Function _1D6() As Integer
Return D(6)
End Function
Public Function _2D6() As Integer
Return D(6) + D(6)
End Function
Public Function _3D6() As Integer
Return D(6) + D(6) + D(6)
End Function
Public Function _4D6() As Integer
Return D(6) + D(6) + D(6) + D(6)
End Function
Public Function CRLF() As String
Return Microsoft.VisualBasic.ControlChars.CrLf
End Function
End Module
And turn it into this (code left out for brevity)
Public NotInheritable Class CoreModule
' Methods
Shared Sub New()
Public Shared Function _1D6() As Integer
Public Shared Function _2D6() As Integer
Public Shared Function _3D6() As Integer
Public Shared Function _4D6() As Integer
Public Shared Function CRLF() As String
Public Shared Function D(ByVal Roll As Integer) As Integer
' Fields
Private Shared R As Random
End Class
In C# the equivalent is this
public sealed class CoreModule
{
// Fields
private static Random R;
// Methods
static CoreModule();
public static int _1D6();
public static int _2D6();
public static int _3D6();
public static int _4D6();
public static string CRLF();
public static int D(int Roll);
}
All that matter is that the emitted CIL does the job correctly.
This capability is the main reason why so many older Visual BASIC 6 programmers are highly annoyed at MS's changes to the language. For example the keyword Integer emitting a Int32 instead of a Int16.
Modules are exposed to other assemblies referencing the original assembly as long as the module is declared public.
Maybe the methods/subs aren't public? I had that problem once, and it would allow access local code in your class, but not if it was outside your class and marked "Private".
Imports System.Web
Imports System.Web.UI
Module ResponseHelper
<System.Runtime.CompilerServices.Extension()> _
Public Sub Redirect(ByVal response As Net.HttpWebResponse, _
ByVal url As String, ByVal target As String, _
ByVal windowFeatures As String)
If String.IsNullOrEmpty(target) Or _
target.Equals("_self", StringComparison.OrdinalIgnoreCase) And _
String.IsNullOrEmpty(windowFeatures) Then
response.Redirect(url, target, windowFeatures)
Else
Dim page As Page = CType(HttpContext.Current.Handler, Page)
If page Is Nothing Then
Throw New InvalidOperationException("Cannot redirect to new window outside Page context.")
End If
url = page.ResolveClientUrl(url)
Dim script As String
If String.IsNullOrEmpty(windowFeatures) Then
script = "window.open(""{0}"", ""{1}"", ""{2}"";"
Else
script = "window.open(""{0}"", ""{1}"");"
End If
script = String.Format(script, url, target, windowFeatures)
ScriptManager.RegisterStartupScript(page, GetType(Page), "Redirect", script, True)
End If
End Sub
End Module
I don't understand what you are asking.
VB.NET does have static classes, just like in C#, because in VB.NET a Module IS a static class. They are one and the same. Anything you can do with a static class you can do with a Module. Perhaps you haven't marked your Public/Private/Protected access correctly?