Using Nim to Creating a vb6 dll that Returns String - vb6

Nim Compiler Version 1.6.6 [Windows: i386]
Compiled at 2022-05-05
Copyright (c) 2006-2021 by Andreas Rumpf
active boot switches: -d:release
Cmd Compile
nim c --cpu:i386 -d:release --app:lib --nomain mydll.nim
Hi there, I was able to return a Long value, now I'm trying to get string values.
I googled to find some exemples and find out here:
https://my.oschina.net/yuekcc/blog/775990
I'm getting this error:
VB6:
Private Declare Function MyStr Lib "mydll.dll" (ByVal s As String) As String
Private Declare Function return_multiply Lib "mydll.dll" Alias "return_multiply#8" (ByVal a As Long, ByVal b As Long) As Long
Private Sub Form_Click()
MsgBox MyStr("?") 'error
MsgBox return_multiply(5, 4) 'ok
End Sub
Another question, why the Alias has #8 at the end? return_multiply#8
Nim:
import encodings
const
vbCodePage = "GB2312"
vbTrue* = 1
vbFalse* = 0
type
VBString* = cstring
VBBoolean* = int32
proc MyStr*(): cstring {.stdcall, exportc, dynlib.} =
result = $"teste"
proc fromVBString*(a: VBString): string =
return encodings.convert($a, "UTF-8", vbCodePage)
proc toVBString*(a: string): VBString =
return VBString(encodings.convert(a, vbCodePage, "UTF-8"))
proc return_multiply*(a, b: int): int {.stdcall, exportc, dynlib.} =
a * b

Since you want to export toVBString in the dynamic library, you have to add the exportc, dynlib pragmas to it as to others:
proc toVBString*(a: string): VBString {.exportc, dynlib, stdcall.} =
return VBString(encodings.convert(a, vbCodePage, "UTF-8"))
But the definition is wrong anyway - I don't know what type VB's String is, but it certainly is different from the Nim string, and I'm not sure why you are importing it in your VB program.
Also, I don't think it's correct to just convert the Nim string to cstring to pass it to VB - Nim's cstring doesn't actually "own" the string data, so when the Nim runtime frees the Nim string, the cstring of it will point to invalid data. I don't know if VB has specific APIs for that or not though.

I know nothing about Nim, but the way to create a VB string is to make an OLE BSTR. SysAllocStringLen() would probably be your best bet. Others in that family might be better depending on what your string data looks like and where it comes from. Check out the MS docs.

Related

Declaring VB6 DLL Functions with two Dimensional Array

I have these arrays:
Dim sInp(10) As String
Dim dInp(90) As Double
Dim sOut(500, 10) As String
Dim dOut(500, 100) As Double
Now I have to declare the following function:
Integer MyFunction(sInp, dInp, dOut, sOut)
How do I do that?
So far I have this Code:
Declare Function MyFunction Lib "C:\Path\To\My\dll.dll" (ByRef sInp() As String, ByRef dInp() As Double, ByRef dOut() As Double, ByRef sOut() As String): Integer
How do I make the last two params to multidimensional arrays?
I already did register the library and the IDE can find the function but I can't use it so I thought I have to declare it like this.
This is all of the documentation I got from the company:
Integer MyFunction(sInp, dInp, dOut, sOut )
sInp[10] (string type)
dInp[90] (double type)
sOut[500, 10] (string type) (The String are initialized as “”)
dOut[500, 100] (double type) (The value are initialized as -9999.99 )
If you say you registered the DLL with "regsvr32" and got a success message then it is an ActiveX DLL (a COM object) and as such you need to instantiate it as you do with any object. First you need to click Project -> References, look for the name of your DLL in the list and click the checkmark beside it. Then you can declare instances from it:
Dim ObjDLL as New WhateverTheDLLClassIsCalled
Then it should expose the "MyFunction" method and you should see its parameters:
Call ObjDll.MyFunction()
The "Declare Function MyFunction Lib" syntax is for regular DLLs that do not need to be registered.

Why is the result of VarPtr(ByVal str) the same as StrPtr(str) ? (VB6)

In VB6 VarPtr should return the address of the variable, in this case the address of the variable str which is allocated on stack, and holds a pointer to a string in memory. StrPtr (or StrPtr) should return the address of the allocated string in the memory. ByVal should just create a copy, but in this case, it works strangely:
Dim str As String
str = "asd"
Debug.Print VarPtr(str)
Debug.Print VarPtr(ByVal str)
Debug.Print StrPtr(str)
The result is:
1636452
110882980
110882980
Why is the result of VarPtr(ByVal str) the same as StrPtr(str) ?
Strings passed ByVal pass the address of the first character of the containing C string in the BStr. StrPtr does the same.
There are two reasons that spring to mind for doing this. Passing Unicode to API calls and string building.
Passing Unicode to API calls
You could use StrPtr on a string rather than a byte array when sending Unicode strings to API functions.
Dim ByteArr() as Byte
Var1="My Text"
ByteArr = Var1
APICall(ByteArr(0))
APICall(StrPtr(Var1))
Should both pass a Unicode string to an API functions. Unicode strings are converted to ANSI strings when using the declare statement as Win 95 didn't do unicode.
String Building
On the other hand if you are string building, then that is built in to VBA using the Left, Right, and Mid statements, not functions (they are overloaded).
Sub Main()
Dim Var As String
Var = "gggggggggggg"
MsgBox StrPtr(Var)
Mid(Var, 1, 2) = "xx"
MsgBox StrPtr(Var) & " - " & Var
End Sub
ByVal Versus ByRef
Some authors like to say that the ByVal keyword is overloaded for
strings, meaning that it takes on a different meaning when applied to
strings than when applied to other variables. Frankly, I don't see it.
Writing:
ByVal str As String
tells VB to pass the contents of the BSTR (actually the ABSTR), which is the pointer to the character array. Thus, ByVal is acting
normally--it just happens that the content of the BSTR is a pointer to
another object, so this simulates a pass by reference. Similarly:
ByRef str As String
passes the address of the BSTR, as expected.
Win32 API Programming with Visual Basic, Chapter 6 Strings, O'Reilly, from MSDN Library October 2001
StrPtr
Strings in Visual Basic are stored as BSTR's. If you use the VarPtr on
a variable of type String, you will get the address of the BSTR, which
is a pointer to a pointer of the string. To get the address of the
string buffer itself, you need to use the StrPtr function. This
function returns the address of the first character of the string.
Take into account that Strings are stored as UNICODE in Visual Basic.
To get the address of the first character of a String, pass the String
variable to the StrPtr function.
Example:
Dim lngCharAddress as Long
Dim strMyVariable as String
strMyVariable = "Some String"
lngCharAddress = StrPtr(strMyVariable)
You can use this function when you need to pass a pointer to a
UNIOCODE string to an API call.
HOWTO: Get the Address of Variables in Visual Basic Q199824 Microsoft Knowledge Base, MSDN October 2001.
VarPtr is not part of the VBA/VB6 language, therefore companies that implement VBA (like Corel) may not implement it in their VBA. The VBA spec is here https://msdn.microsoft.com/en-us/library/dd361851.aspx

BestPractices: Out parameters vs complex return types in methods

Using complex return type:
Public Type TimeType
hours As Integer
minutes As Integer
End Type
Public Function ParseTimeField(time As String) As TimeType
Dim timeObject As TimeType
Dim amountOfDigitHours As Integer
If time = "" Then time = "0"
If HasHoursAndMinutesParts(time) Then
amountOfDigitHours = GetAmountOfDigitHours(time)
timeObject.hours = CInt(Left(time, amountOfDigitHours))
timeObject.minutes = CInt(Right(time, 2))
Else
timeObject.hours = 0
timeObject.minutes = CInt(time)
End If
ParseTimeField = timeObject
End Function
Private Function HasHoursAndMinutesParts(time As String) As Boolean
HasHoursAndMinutesParts = Len(time) > 2
End Function
Private Function GetAmountOfDigitHours(time As String) As Integer
GetAmountOfDigitHours = Len(time) - 2
End Function
Call:
Dim timeObj As TimeType
timeObj = ParseTimeField(strTime)
OR using out parameters:
Public Function ParseTimeField(time As String, ByRef hours As Integer, ByRef minutes As Integer)
Dim timeObject As TimeType
Dim amountOfDigitHours As Integer
If time = "" Then time = "0"
If HasHoursAndMinutesParts(time) Then
amountOfDigitHours = GetAmountOfDigitHours(time)
hours = CInt(Left(time, amountOfDigitHours))
minutes = CInt(Right(time, 2))
Else
hours = 0
minutes = CInt(time)
End If
ParseTimeField = timeObject
End Function
Private Function HasHoursAndMinutesParts(time As String) As Boolean
HasHoursAndMinutesParts = Len(time) > 2
End Function
Private Function GetAmountOfDigitHours(time As String) As Integer
GetAmountOfDigitHours = Len(time) - 2
End Function
Call:
Dim hours As Integer
Dim minutes As Integer
Call ParseTimeField(strTime, hours, minutes)
BTW this is VB6 code =)
If you have a single return type, do not use an out parameter to return it.
In general, I find multiple ref/out parameters to be a code smell. If you need to return data from your method, it is better if it is in one coherent object.
I have a feeling we're going to see different opinions on this matter. Not sure there exists a best practice.
I usually prefer complex datatypes because I feel that is more in line with the original structure of functions where output parameter preceed the input parameters in the signature. Basically I don't like out parameters - they're superfluous. Whenever there's two ways of doing a thing in a programming language, you complicate unneccessary (guess I'm gonna get killed by Perl-fanatics stating this). You return data with the return statement. Period.
This said, I still find myself using out parameters often when I need to return two parameteres that have no natural grouping - i.e. would end up in a class which would be used solely in the return value of this specific function.
Whenever there's three or more parameters in the return data, I never use out simply because I find the calling code to be excessive verbose - needing to Dim the variables (or var'em in C#)
I tend to use out params as the universal style. Rarely need to implement helper functions that return UDT but these are usually private to the module so I can keep the scope of the UDT private to the module too.
In the latter case usually consume the retval like this
With ParseTimeField(strTime)
Debug.Print .hours, .minutes
End With
... and most probably would keep TimeType with private scope.
Definately a matter of opinion. I do use ByRef parameters from time to time, especially for utility functions which require a success/fail type scenario. For example, the TryParse functions in the .net framework do exactly this. Your parametrised function could look like:
Public Function ParseTimeField(time As String, ByRef hours As Integer, ByRef minutes As Integer) As Boolean
'Do stuff'
ParseTimeField = True 'or false depending on output success'
End Sub
Meaning you can call it like:
Dim hours As Integer
Dim mins as Integer
If ParseTimeField(time, hours, mins) = True Then
'It worked'
End If
However as things start to get more complicated, and you're actually returning business items as opposed to doing logical commands, then a separate class and a return type is more desirable. It also makes calling AND returning easier to maintain.

VB6, File Doesn't Exist, How do I handle Gracefully?

I am testing an application that checks if a file exists across a network. In my testing, I am purposefully pulling the network plug so the file will not be found. The problem is this causes my app to go unresponsive for at least 15 seconds. I have used both the FileExists() and GetAttr() functions in VB6. Does anyone know how to fix this problem? (No, I can't stop using VB6)
Thanks,
Charlie
Unfortunately, VB doesn't make this easy, but luckily the Win32 API does, and it's quite simple to call Win32 functions from within VB.
For the LAN/WAN, you can use a combination of the following Win32 API calls to tell you whether the remote connection exists without having to deal with a network time-out:
Private Declare Function WNetGetConnection Lib "mpr.dll" Alias _
"WNetGetConnectionA" (ByVal lpszLocalName As String, _
ByVal lpszRemoteName As String, ByRef cbRemoteName As Long) As Long
Private Declare Function PathIsNetworkPath Lib "shlwapi.dll" Alias _
"PathIsNetworkPathA" (ByVal pszPath As String) As Long
Private Declare Function PathIsUNC Lib "shlwapi.dll" Alias "PathIsUNCA" _
(ByVal pszPath As String) As Long
For the Internet, you can use the Win32 API call:
Private Declare Function InternetGetConnectedState Lib "wininet.dll" _
(ByRef lpdwFlags As Long, ByVal dwReserved As Long) As Long
Const INTERNET_CONNECTION_MODEM = 1
Const INTERNET_CONNECTION_LAN = 2
Const INTERNET_CONNECTION_PROXY = 4
Const INTERNET_CONNECTION_MODEM_BUSY = 8
This VB site has more discussion on path oriented functions you can call in the Win32 API through VB.
use this too
Dim FlSize as long
flsize=filelen("yourfilepath")
if err.number=53 then msgbox("file not found")
if err.number=78 then msgbox("Path Does no Exist")
I'm not sure you can handle this much more gracefully - if the network is having problems it can take a while for timeouts to indicate that the problem is severe enough that things aren't working.
If VB6 supports threading (I honestly don't recall) you could spin the file open into a background thread, and have the UI allow the user to cancel it (or perform other operations if that makes sense), but that introduces a pretty significant amount of additional complexity.
VB6 has some networking functions that can test to see if the network is connected. You should be able to add in under 'References' the 'NetCon 1.0 Type Library'. This adds for you the NETCONLib. Once implemented, you should be able to test for network connectivity first, then test for the FileExists and GetAttr.
Let me know if this helps!
VB is inherently single threaded, but you can divert work to a COM component to do an asynchronous file check and flag an event when it is done. This way the UI thread stays at responsive at least. Trouble is - this is all theory, I don't know such a component.
But wait! Google just turned up this: Visual Basic 6 Asynchronous File I/O Using the .NET Framework. Does that help, maybe?
Also, they have something similar over at CodeProject: Asynchronous processing - Basics and a walkthrough with VB6/ VB.NET
this code only used for check connection (maybe can help you) for one of your problems :
Private Declare Function InternetGetConnectedState Lib "wininet.dll" (ByRef dwFlags As Long, ByVal dwReserved As Long) As Long
Private Const CONNECT_LAN As Long = &H2
Private Const CONNECT_MODEM As Long = &H1
Private Const CONNECT_PROXY As Long = &H4
Private Const CONNECT_OFFLINE As Long = &H20
Private Const CONNECT_CONFIGURED As Long = &H40
Public Function checknet() As Boolean
Dim Msg As String
If IsWebConnected(Msg) Then
checknet = True
Else
If (Msg = "LAN") Or (Msg = "Offline") Or (Msg = "Configured") Or (Msg = "Proxy") Then
checknet = False
End If
End If
End Function
Private Function IsWebConnected(Optional ByRef ConnType As String) As Boolean
Dim dwFlags As Long
Dim WebTest As Boolean
ConnType = ""
WebTest = InternetGetConnectedState(dwFlags, 0&)
Select Case WebTest
Case dwFlags And CONNECT_LAN: ConnType = "LAN"
Case dwFlags And CONNECT_MODEM: ConnType = "Modem"
Case dwFlags And CONNECT_PROXY: ConnType = "Proxy"
Case dwFlags And CONNECT_OFFLINE: ConnType = "Offline"
Case dwFlags And CONNECT_CONFIGURED: ConnType = "Configured"
Case dwFlags And CONNECT_RAS: ConnType = "Remote"
End Select
IsWebConnected = WebTest
End Function
in your event :
If checknet = False Then
...
else
...
end if
I agree with Will. Something like this is simple to handle with Script.FileSystemObject:
Dim objFSO As New FileSystemObject
If objFSO.FileExists("C:\path\to\your_file.txt") Then
' Do some stuff with the file
Else
' File isn't here...be nice to the user.
EndIf
Accessing files over a network can cause these hangs.
It's been a while, but I remember multi-threading in VB6 being relatively painful to implement. A quick solution would be to have a small .exe (perhaps also coded in VB) that can handle this. You could use DDE for inter-app communication or the ever so easy but kludgey file-based pipe, by which I mean a file that both apps will mutually read/write to handle inter-app communication. Of course, using file-based pipes, depending on the details of this scenario, may simply exaggerate the File I/O lag.
If there's a reasonable degree with which you can predict where the user will be selecting files from, you may consider preemptively caching a directory listing and reading that rather than the file directly - assuming the directory contents aren't expected to change frequently. Note: getting a directory listing over a network will cause the same lag issues as individual file I/O over a network. Keep that in mind.

What's the best way to determine if a character is a letter in VB6?

Need a function that takes a character as a parameter and returns true if it is a letter.
This was part of the code posted by rpetrich in response to a question by Joel Spolsky. I felt it needed a post specific to the problem it solves. It really is brilliant.
Private Function IsLetter(ByVal character As String) As Boolean
IsLetter = UCase$(character) <> LCase$(character)
End Function
You may be thinking to yourself, "Will this always work?" The documentation on the UCase and LCase functions, confirms that it will:
UCase Function Only lowercase letters are converted to uppercase;
all uppercase letters and nonletter characters remain unchanged.
LCase Function Only uppercase letters are converted to lowercase;
all lowercase letters and nonletter characters remain unchanged.
Seanyboy's IsCharAlphaA answer is close. The best method is to use the W version like so:
Private Declare Function IsCharAlphaW Lib "user32" (ByVal cChar As Integer) As Long
Public Property Get IsLetter(character As String) As Boolean
IsLetter = IsCharAlphaW(AscW(character))
End Property
Of course, this all rarely matters as all of VB6's controls are ANSI only
Private Function IsLetter(Char As String) As Boolean
IsLetter = UCase(Char) Like "[ABCDEFGHIJKLMNOPQRSTUVWXYZ]"
End Function
What's wrong with the following, which doesn't rely on obscure language behaviour?
Private Function IsLetter(ByVal ch As String) As Boolean
IsLetter = (ch >= "A" and ch <= "Z") or (ch >= "a" and ch <= "z")
End Function
I believe we can improve upon this a little more. rpetrich's code will work, but perhaps only by luck. The API call's parameter should be a TCHAR (WCHAR here actually) and not a Long. This also means no fiddling with converting to a Long or masking with &HFFFF. This by the way is Integer and adds an implicit conversion to Long here too. Perhaps he meant &HFFFF& in this case?
On top of that it might be best to explictly call the UnicoWS wrapper for this API call, for Win9X compatibility. The UnicoWS.dll may need to be deployed but at least we gain that option. Then again maybe from VB6 this is automagically redirected, I don't have Win9X installed to test it.
Option Explicit
Private Declare Function IsCharAlphaW Lib "unicows" (ByVal WChar As Integer) As Long
Private Function IsLetter(Character As String) As Boolean
IsLetter = IsCharAlphaW(AscW(Character))
End Function
Private Sub Main()
MsgBox IsLetter("^")
MsgBox IsLetter("A")
MsgBox IsLetter(ChrW$(&H34F))
MsgBox IsLetter(ChrW$(&HFEF0))
MsgBox IsLetter(ChrW$(&HFEFC))
End Sub
Looking around a bit came up with the following...
Private Declare Function IsCharAlphaA Lib "user32" Alias "IsCharAlphaA" (ByVal cChar As Byte) As Long
I believe IsCharAlphaA tests ANSI character sets and IsCharAlpha tests ASCII. I may be wrong.
Private Function IsAlpha(ByVal vChar As String) As Boolean
Const letters$ = "abcdefghijklmnopqrstuvwxyz"
If InStr(1, letters, LCase$(vChar)) > 0 Then IsAlpha = True
End Function
I use this in VBA
Function IsLettersOnly(Value As String) As Boolean
IsLettersOnly = Len(Value) > 0 And Not UCase(Value) Like "*[!A-Z]*"
End Function
It doesn't exactly document itself. And it may be slow. It's a clever hack, but that's all it is. I'd be tempted to be more obvious in my checking. Either use regex's or write a more obvious test.
public bool IsAlpha(String strToCheck)
{
Regex objAlphaPattern=new Regex("[^a-zA-Z]");
return !objAlphaPattern.IsMatch(strToCheck);
}
public bool IsCharAlpha(char chToCheck)
{
return ((chToCheck=>'a') and (chToCheck<='z')) or ((chToCheck=>'A') and (chToCheck<='Z'))
}

Resources