Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Hello everyone,
I'm dealing with a compatibility issue between VB6 and the Speech Application Programming Interface (SAPI), specifically regarding the 32-bit and 64-bit versions of SAPI.
Here's the scenario:
SAPI Versions: There are two versions of SAPI, one for 32-bit applications and another for 64-bit applications.
Compatibility Constraints:
A 32-bit executable (exe) can only interact with the 32-bit version of SAPI.
A 64-bit executable is required to interact with the 64-bit version of SAPI.
As a voice vendor, I create voices using a C++ COM DLL that interfaces with SAPI. Within this voice DLL, I incorporate MKLML alongside ONNX Runtime to enhance performance. Unfortunately, MKLML is not available in a 32-bit version, limiting me to producing only the 64-bit variant of my voice.dll.
Problem: Since SAPI 32-bit can only interact with 32-bit DLLs, my VB6 application, which is inherently 32-bit, cannot use the 64-bit voice.dll.
Question: Is there still no way for a VB6 application to interact with the 64-bit version of SAPI?
From what I understand, there isn't a method to bridge this gap directly due to the architectural differences between 32-bit and 64-bit applications and their respective DLLs. However, I wanted to ask here in case anyone has come across a workaround or solution that I might have missed.
Thank you for your insights!
Last edited by tmighty2; Aug 2nd, 2024 at 05:07 PM.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
I'd probably write a small C++ stub program that actually made the API calls into your Voice.DLL. And this C++ program would work like a no-user-interface server. Then, you'd need to establish a way you could do inter-process communications with this C++ program. I'd probably use subclassed hidden (always loaded) windows on both sides, and then use SendMessageTimeout to send messages between those two windows.
Done that way, you could set up a protocol where VB6 could "tell" the C++ program to make API calls. And, when those API calls returned, the C++ program could send the results back to your VB6 program.
I'll let you sort out the subclassing, but here's a piece of code I'd use to send the messages from the VB6 side. It'd be the same on the C++ side, just written in C++.
Code:
Option Explicit
'
Private Type COPYDATASTRUCT
dwData As Long
cbData As Long
lpData As Long
End Type
'
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Dest As Any, ByRef Source As Any, ByVal Bytes As Long)
Private Declare Function SendMessageTimeout Lib "user32" Alias "SendMessageTimeoutA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any, ByVal fuFlags As Long, ByVal uTimeout As Long, lpdwResult As Long) As Long
'
Public Sub SendStringToAnotherWindow(hWndSender As Long, hWndTarget As Long, sMsg As String)
' This can be used to send a message (string) to another window, possibly in another VB6 program.
' The other VB6 program MUST be expecting the message. And it will need to be subclassed (i.e., hooked).
' See the StringMessageHook, StringMessageUnhook, and StringMessageWindowProc for details on how
' the receiving program must be set up.
'
Dim cds As COPYDATASTRUCT
Dim lpdwResult As Long
Dim Buf() As Byte
Const WM_COPYDATA = &H4A
'
If hWndTarget Then
ReDim Buf(1 To Len(sMsg) + 1)
Call CopyMemory(Buf(1), ByVal sMsg, Len(sMsg)) ' Copy the string into a byte array, converting it to ASCII.
cds.dwData = 3
cds.cbData = Len(sMsg) + 1
cds.lpData = VarPtr(Buf(1))
'Call SendMessage(hWndTarget, WM_COPYDATA, Me.hwnd, cds)
SendMessageTimeout hWndTarget, WM_COPYDATA, hWndSender, cds, 0, 5000, lpdwResult ' Return after 5 seconds even if receiver didn't acknowledge.
End If
End Sub
Upon loading things up, you'd also probably have to do some FindWindow work, putting titles in these hidden windows and then each side "finding" the other side's hidden window's handle.
But, all said and done, that shouldn't be too difficult. It's basically pretty close to creating your own home-grown "automation".
Last edited by Elroy; Aug 3rd, 2024 at 07:01 AM.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Originally Posted by tmighty2
Elroy, thank you, that is really a possible solution.
There'd still be lots of little details to consider, for instance:
I'd only want one copy of this VB6 program running at any specific time, so check App.PrevInstance.
VB6 would probably start up the C++ "bridge" program. So, it'd need to check if there's already a copy of it running and just use that copy if it's already loaded. If not, then the VB6 program would need to load the C++ program (probably a simple Shell command).
Is it ok if the C++ program just runs all the time? If not, you might want some kind of timer in it to occasionally check and make sure the VB6 program is still running ... and, if not, terminate.
Once you get into it, there will almost certainly be other minor details. But this is certainly doable.
Regarding using twinBasic for the "bridge" program, I'm not sure what it'd take (in terms of licensing twinBasic to get a compiled "bridge" program). But, if that's an option, and you'd rather keep it all written in a VB-ish language, that does sound like a good option. However, I'll leave it to you to look into that.
Truth be told, any language that'll compile to 64-bit code could be used for your "bridge" program. You'd certainly want it compiled to machine language though so your "bridge" program didn't cause any slowdown.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
VB6 could talk to a 64-Bit COM DLL, but not standard 64-Bit DLL. The API version of CreateObject() allows creating out-of-process objects using DLLHOST.exe. You can talk to 32 or 64 Bit COM DLL's that way. The OS takes care of moving data back and forth between VB6 and the 64-Bit COM DLL. Please see CreateObject64() in post #6 in this thread and my subsequent posts.
In my case, I made a 64-Bit COM DLL using C++(Not SAPI related), and talked to it from VB6. Windows runs the 64-Bit version of DLLHOST.exe to host the 64-Bit DLL.
Another option for you is to turn your DLL into 64-Bit COM EXE(Same as ActiveX EXE). This doesn't require adding registry entries like the 64-Bit DLL option.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Originally Posted by Elroy
Regarding using twinBasic for the "bridge" program, I'm not sure what it'd take (in terms of licensing twinBasic to get a compiled "bridge" program). But, if that's an option, and you'd rather keep it all written in a VB-ish language, that does sound like a good option. However, I'll leave it to you to look into that.
Truth be told, any language that'll compile to 64-bit code could be used for your "bridge" program. You'd certainly want it compiled to machine language though so your "bridge" program didn't cause any slowdown.
Community edition is free but puts a splash screen on initial loads of 64bit exes/dlls, however no restrictions on language features or project types besides llvm-optimized binaries (partially complete). 'VB-ish language' makes it sound like b4a or powerbasic or some other incompatiible dialect; to be clear, its the VB6 language exactly as a compatibility base; it's like VB1-5 vs 6; not another language vs vb6. New syntax and language features are opt in. It runs existing VB6 code without modification in most cases (minus a few bugs, minor unimplemented features, and vb6 internals hacks easily replaced with simpler techniques). For 64bit it uses the MS Office VBA7 syntax with LongPtr/PtrSafe.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Originally Posted by fafalone
'VB-ish language' makes it sound,,,
Didn't mean to imply anything negative. All I was trying to convey is that twinBasic isn't VB. But, it's certainly VB-ish.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Thank you for the recommendation to explore DLL Surrogates as a solution for accessing 64-bit functionalities in my VB6 application. I am attempting to implement this solution to access 64-bit SAPI voices and would appreciate your detailed feedback on my current approach and setup.
Background and Current Implementation:
I have developed a VB6 application (MyApp.exe) that needs to utilize the advanced features available in the 64-bit version of the Speech API (sapi.dll).
Based on the suggestion, I've encapsulated the SAPI functionalities within an ActiveX DLL, hoping to leverage it as a surrogate for bridging the 32-bit and 64-bit divide.
ActiveX DLL Creation (SpVoiceAsActiveXDLL.dll):
I've created an ActiveX DLL that hosts the SpVoice object from the SpeechLib library.
This DLL is designed to function as a surrogate, aiming to offload the speech processing tasks to the 64-bit sapi.dll.
Code:
' Class within the ActiveX DLL
Class cSpVoiceInActiveXDLL
Private WithEvents m_Voice As SpeechLib.SpVoice
Public Sub Speak(ByVal uText As String)
Dim lStream&
lStream = m_Voice.Speak(uText, SVSFlagsAsync Or SVSFIsXML)
End Sub
Integration in VB6 Application (MyApp.exe):
In the main VB6 application, I replaced direct instances of SpVoice with instances of the class from the ActiveX DLL.
Code:
' Usage in MyApp.exe
Private WithEvents MyVoice As New cSpVoiceInActiveXDLL
MyVoice.Speak("Hi!")
Points Needing Clarification:
A. Correct Implementation of DLL Surrogates:
Is my approach in structuring the ActiveX DLL as a surrogate correct for accessing 64-bit features from a 32-bit application?
Does this setup align with the recommended use of DLL Surrogates to handle cross-bit operations effectively?
B. Registry and Configuration Requirements:
What specific entries or modifications are necessary in the Windows Registry to ensure that my ActiveX DLL functions as a proper DLL Surrogate?
Are there additional configuration steps required to fully enable the interaction between the 32-bit VB6 environment and the 64-bit DLL?
[*]C. Potential Misunderstandings and Corrections:
If there are any misconceptions in my current understanding or implementation, what corrections would you suggest?
I am committed to making this integration work and would immensely value any step-by-step guidance, insights, or illustrative examples you could provide to help refine my implementation.
Thank you for your continued support and assistance!
Last edited by tmighty2; Aug 4th, 2024 at 07:32 PM.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
This is such a mess. I could even write a 64bit VB.NET app for this which can see the 64bit voices and call them, and I would use a textbox in both the VB.NET 64 bit app and in my VB6 32 bit app and a timer, and each app watches the other app's textbox for a new message.
LOL. I guess that is about the same what pipes would be able to do for me, right?
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
ActiveX is a marketing term that Microsoft no longer use, they simply renamed it to COM. It's the same thing. So I use them interchangeable.
Making a VB6 ActiveX/COM DLL doesn't work, because it's always 32-Bit.
There are 2 general options:
1 - Preferred: Make a 64-Bit ActiveX/COM EXE. This would host SAPI and any other 64-Bit tech, and talk to it from your standard VB6 EXE, and yes, you can use WithEvents. If using C++(without .Net), the wizard can create a COM project for you, you just have to copy-paste the functions you need. I think you don't need my CreateObject64 with this, but I am not sure.
2 - Make a 64-Bit ActiveX/COM DLL. This doesn't make sense in most cases, but I had a rare use for it. This 64-Bit DLL doesn't run on its own, it requires a 64-Bit EXE, which is DLLHOST.exe. It requires adding registry entries, and using my CreateObject64. Please use option 1 above. I mentioned this option for completeness sake.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
What's the problem with DLLSurrogate? It's the reliable solution. If you want to use an external 64 bit application you just can create the object and pass the moniker (for example ObjRef moniker) to 32 bit application.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Btw, I just failed using PutObject from linked thread above in an x64 target in TB and then GetObject from VB6 which is weird. Same code with x86 target was working fine in VB6 as expected. Both processes were running elevated which might have been a problem with x64 server in TB.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Originally Posted by tmighty2
Can you give me really quick guide how to make such a "64 bit ActiveX/COM EXE" using VS2022 / C++?
I am asking because I don't understand how that would be different to a ATL COM DLL.
Let's find out why DLLSurrogate isn't suitable for you? The #4 post contains the links which describe how to register COM server (SAPI in your case) to run in the surrogate process (dllhost).
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Originally Posted by tmighty2
Can you give me really quick guide how to make such a "64 bit ActiveX/COM EXE" using VS2022 / C++?
I am asking because I don't understand how that would be different to a ATL COM DLL.
ATL = Active Template Library, which is the same as ActiveX, and probably how MS got the ActiveX name.
However, The trick is suggesting a 3rd option. Use 64-Bit SAPI COM "directly" by using CreateObject64 to create SAPI objects, they would be hosted by the surrogate process(DLLHOST). There is no need to create a separate 64-Bit DLL or EXE in this case.
If you have anything extra you want to talk to that is 64-Bit and doesn't have COM interface, then you have to create 64-Bit COM EXE to host it.
Edit: I didn't consider what The trick suggested because you also mentioned MKLML and ONNX. I don't know what they are, or if they have COM interface.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Can you please confirm that this is how I am supposed to do it?
Code:
Option Explicit
Private WithEvents m_Voice As SpeechLib.SpVoice
Private Sub Form_Load()
Dim bTry64Bit As Boolean
bTry64Bit = True
If bTry64Bit Then
Set m_Voice = CreateObject64("SAPI.SpVoice")
'this shows the error message "CLSIDFromString failed, ret = 800401F3, LastDllError = 14007"
Else
Set m_Voice = CreateObject("SAPI.SpVoice")
End If
When I set the CLSID manually....
Code:
Public Function CreateObject64() As Object
Dim ret As Long
Dim pCLSID As TGUID
Dim IIDispatch As TGUID
Dim ppv As Long
' Convert IID_IDispatch to a TGUID
ret = CLSIDFromString(StrPtr(IID_IDispatch), IIDispatch)
If ret <> 0 Then
MsgBox "CLSIDFromString for IID_IDispatch failed, ret = " & Hex(ret)
Exit Function
End If
' Manually specify CLSID for SAPI.SpVoice
ret = CLSIDFromString(StrPtr("{96749377-3391-11D2-9EE3-00C04F797396}"), pCLSID)
If ret = 0 Then
' Create Object
ret = CoCreateInstance(VarPtr(pCLSID), 0, CLSCTX_LOCAL_SERVER, VarPtr(IIDispatch), ppv)
If ppv <> 0 Then
Set CreateObject64 = ObjFromPtr(ppv)
Else
Debug.Print "CoCreateInstance failed, ppv is zero. ret was: " & ret
'CoCreateInstance failed, ppv is zero. ret was: -2147221164 'REGDB_E_CLASSNOTREG
End If
Else
MsgBox "CLSIDFromString for SAPI.SpVoice failed, ret = " & Hex(ret)
End If
End Function
... then CoCreateInstance fails with -2147221164 (REGDB_E_CLASSNOTREG, I think)
Last edited by tmighty2; Aug 5th, 2024 at 05:25 PM.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
I had problems with the declaration of CLSIDFromProgID(), that's why it's not finding the key. Fixed below, but CoCreateInstance() instance is failing, possibly because I didn't add this registry entry under AppID:
I am leaving so I hope that someone else will get it working.
Code:
Option Explicit
Public Type TGUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
Private Enum CLSCTX
CLSCTX_INPROC_SERVER = &H1
CLSCTX_INPROC_HANDLER = &H2
CLSCTX_LOCAL_SERVER = &H4
CLSCTX_INPROC_SERVER16 = &H8
CLSCTX_REMOTE_SERVER = &H10
CLSCTX_INPROC_HANDLER16 = &H20
CLSCTX_RESERVED1 = &H40
CLSCTX_RESERVED2 = &H80
CLSCTX_RESERVED3 = &H100
CLSCTX_RESERVED4 = &H200
CLSCTX_NO_CODE_DOWNLOAD = &H400
CLSCTX_RESERVED5 = &H800
CLSCTX_NO_CUSTOM_MARSHAL = &H1000
CLSCTX_ENABLE_CODE_DOWNLOAD = &H2000
CLSCTX_NO_FAILURE_LOG = &H4000
CLSCTX_DISABLE_AAA = &H8000
CLSCTX_ENABLE_AAA = &H10000
CLSCTX_FROM_DEFAULT_CONTEXT = &H20000
CLSCTX_ACTIVATE_X86_SERVER = &H40000
CLSCTX_ACTIVATE_32_BIT_SERVER = &H80000
CLSCTX_ACTIVATE_64_BIT_SERVER = &H100000
CLSCTX_ENABLE_CLOAKING = &H200000
CLSCTX_APPCONTAINER = &H400000
CLSCTX_ACTIVATE_AAA_AS_IU = &H800000
CLSCTX_RESERVED6 = &H1000000
CLSCTX_ACTIVATE_ARM32_SERVER = &H2000000
CLSCTX_PS_DLL = &H4000000
End Enum
'Public Const CLSCTX_INPROC_SERVER As Long = 1
'Public Const CLSCTX_INPROC_HANDLER As Long = 2
'Public Const CLSCTX_LOCAL_SERVER As Long = 4
'Public Const CLSCTX_ENABLE_AAA As Long = &H10000
'Public Const CLSCTX_REMOTE_SERVER As Long = 16
'Public Const CLSCTX_SERVER As Long = (CLSCTX_INPROC_SERVER Or CLSCTX_LOCAL_SERVER)
'Public Const CLSCTX_ALL As Long = (CLSCTX_INPROC_SERVER Or CLSCTX_INPROC_HANDLER Or CLSCTX_LOCAL_SERVER)
Public Const IID_IUnknown As String = "{00000000-0000-0000-C000-000000000046}"
Public Const IID_IDispatch As String = "{00020400-0000-0000-C000-000000000046}"
Public Declare Function CoCreateInstance Lib "OLE32.DLL" (ByVal rclsid As Long, ByVal punkOuter As Long, ByVal dwClsContext As Long, ByVal riid As Long, ByRef ppv As Any) As Long
Public Declare Function CLSIDFromString Lib "OLE32.DLL" (ByVal lpsz As Long, ByRef pCLSID As TGUID) As Long
Public Declare Function CLSIDFromProgID Lib "OLE32.DLL" (ByVal TSzProgID As Long, ByRef pCLSID As TGUID) As Long
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As Long)
Public Function CreateObject64(ByRef sObjectName As String) As Object
Dim ret As Long
Dim pCLSID As TGUID
Dim IIDispatch As TGUID
Dim ppv As Long
ret = CLSIDFromString(StrPtr(IID_IDispatch), IIDispatch)
' Get object by GUID
'ret = CLSIDFromString(StrPtr("{11111111-2222-3333-4444-555555555555}"), pCLSID)
'ret = CLSIDFromString(StrPtr("{96749377-3391-11D2-9EE3-00C04F797396}"), pCLSID)
' Get object by ProgID
ret = CLSIDFromProgID(StrPtr(sObjectName), pCLSID)
If ret = 0 Then
' Success
' Create Object
ret = CoCreateInstance(VarPtr(pCLSID), 0, CLSCTX_LOCAL_SERVER Or CLSCTX_ACTIVATE_64_BIT_SERVER, VarPtr(IIDispatch), ppv)
Debug.Print "CoCreateInstance returned = " & Hex(ret) & ", ppv = " & Hex(ppv) & ", LastDllError = " & Err.LastDllError
If ppv <> 0 Then
Set CreateObject64 = ObjFromPtr(ppv)
End If
Else
Debug.Print "CLSIDFromString/CLSIDFromProgID failed, ret = " & Hex(ret) & ", LastDllError = " & Err.LastDllError
End If
End Function
' Returns an object given its pointer
' This function reverses the effect of the ObjPtr function
Public Function ObjFromPtr(ByVal pObj As Long) As Object
Dim obj As Object
' force the value of the pointer into the temporary object variable
CopyMemory obj, pObj, 4
' assign to the result (this increments the ref counter)
Set ObjFromPtr = obj
' manually destroy the temporary object variable
' (if you omit this step you'll get a GPF!)
CopyMemory obj, 0&, 4
End Function
Public Sub Main()
Dim o As Object
Set o = CreateObject64("SAPI.SpVoice")
If o Is Nothing Then
Debug.Print "CreateObject64 Failed."
Else
Debug.Print "CreateObject64 Succeeded."
End If
Set o = Nothing
End Sub
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
For example. If you want to have SAPI.SpVoice class created in the 64bit system surrogate process you should do the following steps:
Get the CLSID of SAPI.SpVoice class that is {96749377-3391-11D2-9EE3-00C04F797396}:
Create AppID entry with the empty DllSurrogate value:
Add the AppID value under corresponding CLSID entry:
Specify CLSCTX_LOCAL_SERVER flag (and optionally CLSCTX_ACTIVATE_64_BIT_SERVER if you have 2 versions of surrogates) and create object:
Code:
Option Explicit
Private Sub Form_Load()
Dim tCLSID As UUID
Dim tIID As UUID
Dim cObj As ISpeechVoice
Dim hr As Long
CLSIDFromString "{96749377-3391-11D2-9EE3-00C04F797396}", tCLSID ' // CLSID_SPVoice
CLSIDFromString "{269316D8-57BD-11D2-9EEE-00C04F797396}", tIID ' // IID_ISpeechVoice
hr = CoCreateInstance(tCLSID, Nothing, CLSCTX_LOCAL_SERVER, tIID, cObj)
If (hr < 0) Then
MsgBox "error " & hr
Exit Sub
End If
cObj.volume = 5
End Sub
The 64 bit COM server is loaded to external 64 bit system surrogate process (DllHost.exe):
In order to verify you can check the list of modules:
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Btw, PutObject/GetObject file monikers approach works using x64 TwinBASIC stub -- no registry tweaking required, works with every ProgID you need x64 instance of.
Here is TB source of the x64 proxy exe
Code:
'--- Form1
[Description("")]
[FormDesignerId("161BDD2F-DFC8-42D6-9BCD-4097FE740594")]
[PredeclaredId]
Class Form1
Option Explicit
#If Win64 Then
Private Declare PtrSafe Function CreateFileMoniker Lib "ole32" (ByVal lpszPathName As LongPtr, pResult As IUnknown) As Long
Private Declare PtrSafe Function GetRunningObjectTable Lib "ole32" (ByVal dwReserved As Long, pResult As IUnknown) As Long
Private Declare PtrSafe Function DispCallFunc Lib "oleaut32" (ByVal pvInstance As LongPtr, ByVal oVft As Long, ByVal lCc As Long, ByVal vtReturn As VbVarType, ByVal cActuals As Long, prgVt As Any, prgpVarg As Any, pvargResult As Variant) As Long
Private Const PTR_SIZE As Long = 8
#Else
Private Enum LongPtr
[_]
End Enum
Private Declare Function CreateFileMoniker Lib "ole32" (ByVal lpszPathName As LongPtr, pResult As IUnknown) As Long
Private Declare Function GetRunningObjectTable Lib "ole32" (ByVal dwReserved As Long, pResult As IUnknown) As Long
Private Declare Function DispCallFunc Lib "oleaut32" (ByVal pvInstance As LongPtr, ByVal oVft As Long, ByVal lCc As Long, ByVal vtReturn As VbVarType, ByVal cActuals As Long, prgVt As Any, prgpVarg As Any, pvargResult As Variant) As Long
Private Const PTR_SIZE As Long = 4
#End If
Private m_lCookie As Long
Public Function CreateObject(sProgID As String) As Object
Set CreateObject = VBA.CreateObject(sProgID)
End Function
Private Sub Form_Load()
m_lCookie = PutObject(Me, "MyApp.MyProxy")
End Sub
Private Sub Form_Unload(Cancel As Integer)
RevokeObject m_lCookie
End Sub
Private Function PutObject(oObj As Object, sPathName As String) As Long
Const ROTFLAGS_REGISTRATIONKEEPSALIVE As Long = 1
Const IDX_REGISTER As Long = 3
Dim pROT As IUnknown
Dim pMoniker As IUnknown
Call GetRunningObjectTable(0, pROT)
Call CreateFileMoniker(StrPtr(sPathName), pMoniker)
DispCallByVtbl pROT, IDX_REGISTER, ROTFLAGS_REGISTRATIONKEEPSALIVE, ObjPtr(oObj), ObjPtr(pMoniker), VarPtr(PutObject)
End Function
Private Sub RevokeObject(ByVal lCookie As Long)
Const IDX_REVOKE As Long = 4
Dim pROT As IUnknown
Call GetRunningObjectTable(0, pROT)
DispCallByVtbl pROT, IDX_REVOKE, lCookie
End Sub
Private Function DispCallByVtbl(pUnk As IUnknown, ByVal lIndex As Long, ParamArray A() As Variant) As Variant
Const CC_STDCALL As Long = 4
Dim lIdx As Long
Dim vParam() As Variant
Dim vType(0 To 63) As Integer
Dim vPtr(0 To 63) As LongPtr
Dim hResult As Long
vParam = A
For lIdx = 0 To UBound(vParam)
vType(lIdx) = VarType(vParam(lIdx))
vPtr(lIdx) = VarPtr(vParam(lIdx))
Next
hResult = DispCallFunc(ObjPtr(pUnk), lIndex * PTR_SIZE, CC_STDCALL, vbLong, lIdx, vType(0), vPtr(0), DispCallByVtbl)
If hResult < 0 Then
Err.Raise hResult
End If
End Function
End Class
Here is VB6 code
Code:
'--- Form1
Option Explicit
Private m_oProxy64 As Object
Private Sub Form_Load()
Set m_oProxy64 = GetObject("MyApp.MyProxy")
End Sub
Private Sub Form_Click()
Dim oSapi As Object
On Error GoTo EH
Set oSapi = m_oProxy64.CreateObject("SAPI.SpVoice")
MsgBox TypeName(oSapi)
Exit Sub
EH:
MsgBox Err.Description, vbCritical
End Sub
Of course TB's source can be cleaned up using regular interfaces (which the language now supports) and then can be implemented to parse command line for moniker name, etc.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
@TheTrick I get permission denied error trying to create AppID in Computer\HKEY_CLASSES_ROOT\CLSID\{96749377-3391-11D2-9EE3-00C04F797396}.
I am going to test how to resolve this.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Thank you. I have chosen the "Standard EXE" in tb.
For project name I used "tbTry3", Option Explicit on, Visual Styles on, Include common controls on, dpi awareness System_DPI_Aware.
Then I clicked "Form1.twin.
It says:
Code:
[Description("")]
[FormDesignerId("257B7201-9C32-487E-93BF-C2B3B97AFBFF")]
[PredeclaredId]
Class Form1
Sub New()
End Sub
End Class
I noticed my FormDesignerId is different than yours.
I replaced the code with your code.
Trying to compile, I get:
[DEBUGGER] global variables have been cleared
[BUILD] Starting...
[LINKER] compilation (codegen) error detected in 'Form1.{default_constructor}' at line #1
[TYPELIB] failed to create typeinfo for coclass 'Form1'. Class contains compilation errors.
[LINKER] FAILED to create type library
[BUILD] failed
I replace your FormDesignerId with the one that was present in my version.
Now it compiles saying
[BUILD] Starting...
02:48:52.638
[LINKER] SUCCESS created output file 'C:\Users\MyUser\Desktop\tbTry3proj\Build\tbTry3_win64.exe'
02:48:52.638
[LINKER]
Launch EXE
Open Folder
I guess I have to start this file to be able to use it, so I double-click it, and it shows the tb splash screen.
I click the form you provided me with, and it works.
Can you tell me how to use the SPVoice withevents?
I have modified your VB6 Form code like that, expecting events to occur, but they do not:
Code:
'--- Form1
Option Explicit
Private m_oProxy64 As Object
Private WithEvents m_Voice As SpeechLib.SpVoice
Private Sub Form_Load()
Set m_oProxy64 = GetObject("MyApp.MyProxy")
End Sub
Private Sub Form_Click()
Dim oSapi As Object
On Error GoTo EH
Set m_Voice = m_oProxy64.CreateObject("SAPI.SpVoice")
Dim lFlags&
lFlags = 1 'async
Dim lStream&
lStream = m_Voice.Speak("this is a test", lFlags)
Exit Sub
EH:
MsgBox Err.Description, vbCritical
End Sub
Private Sub m_voice_StartStream(ByVal StreamNumber As Long, ByVal StreamPosition As Variant)
Debug.Print "start " & StreamNumber
End Sub
Private Sub m_voice_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
Debug.Print "word streamnumber: " & StreamNumber
End Sub
Edit: I am using polling with a Timer now. It does work fine. However, if there should be any way to get events the "usual way" I would appreciate knowing about it.
Thank you so much! I have working solution.
Last edited by tmighty2; Aug 6th, 2024 at 09:21 PM.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Originally Posted by tmighty2
@TheTrick I get permission denied error trying to create AppID in Computer\HKEY_CLASSES_ROOT\CLSID\{96749377-3391-11D2-9EE3-00C04F797396}.
I am going to test how to resolve this.
You should change owner from TrustedInstaller to your account.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
I noticed that the events are difficult to handle as I required multiple instance of SPVoice at the same time.
Doing this in TB would not get me the events in TB:
Code:
Public Function CreateObject(sProgID As String) As Object
Set CreateObject = VBA.CreateObject(sProgID)
End Function
Instead, I would (IMO) have to do this in TB:
Code:
Private WithEvents m_Voice As SpVoice
Public Function CreateSAPIVoice() As SpeechLib.SpVoice
Set m_Voice = New SpVoice
Set CreateSAPIVoice = m_Voice
End Function
This way, the TB form will receive the SPVoice events, and I can use a Timer to poll the TB.
However, I need multiple instances of SPVoice at the same time.
I store them in a collection.
This is how I tried to do it in TB:
Code:
Private WithEvents m_Voices() As SpVoice
Public Function GetAnotherSAPIVoice() As SpeechLib.SpVoice
Dim nVoice As SpVoice
Set nVoice = New SpVoice
Dim lUB As Integer = UBound(m_Voices)
ReDim Preserve m_Voices(0 To lUB)
Set m_Voices(lUB) = nVoice
Set GetAnotherSAPIVoice = nVoice
End Function
This works, but the events appear as if it was just one instance:
Code:
Private WithEvents m_Voices() As SpVoice
Private WithEvents m_Voice As SpVoice
Private Sub m_Voice_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
End Sub
Private Sub m_Voices_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
End Sub
Even though one event is for a single instance and the other event is for an array of instances, they look perfectly the same. I think an index is missing to be able to determine which one of the instances raised this event.
There is no "Index As Integer" in the sub, so when an event occurs, I can not determine which one of the m_Voices() raised this event.
I would appreciate help with the events very much.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
> I would appreciate help with the events very much.
There is no reason for events not to work with this proxy setup per se. Here is my test VB6 code
Code:
'--- Form1
Option Explicit
Private Declare Function GetCurrentThreadId Lib "kernel32" () As Long
Private m_oProxy64 As Object
Private WithEvents m_Voice As SpeechLib.SpVoice
Private WithEvents m_oFont As StdFont
Private Sub Command1_Click()
On Error GoTo EH
If m_oProxy64 Is Nothing Then
Set m_oProxy64 = GetObject("MyApp.MyProxy")
End If
If m_oFont Is Nothing Then
Set m_oFont = m_oProxy64.CreateObject("StdFont")
End If
m_oFont.Size = m_oFont.Size + 2
Print "m_oFont.Size " & m_oFont.Size, GetCurrentThreadId
Exit Sub
EH:
MsgBox Err.Description, vbCritical
Set m_oFont = Nothing
Set m_oProxy64 = Nothing
End Sub
Private Sub m_oFont_FontChanged(ByVal PropertyName As String)
Print "m_oFont_FontChanged " & PropertyName, GetCurrentThreadId
End Sub
This works for StdFont instances and FontChanged event is received from x64 instance.
You don't have to use New to receive events i.e. CreateObject works fine as long as your variables is declared WithEvents ... As Strong.Type for early-bound access.
FYI, this is early-bound:
Dim oFont As StdFont
Set oFont = CreateObject("StdFont")
This is late-bound:
Dim oFont As Object
Set oFont = New StdFont
This is early-bound too:
Dim oFont As StdFont
Set oFont = New StdFont
. . . so it does not matter if instance is created with New or CreateObject, it only matter how the variable is dimensioned i.e. As Strong.Type vs As Object. This is has always been the case, it is not something in connection with x64 vs x86 instancing we are discussing here.
Anyway, obviously there is some incompatibility with SAPI.SpVoice in the way events are fired. Probably connected with threading/async implementation which boggles cross-process bridge OS is providing.
Just tested x86 build of the proxy project -- events don't fire cross-process too.
About Private WithEvents m_Voices() As SpVoice syntax -- obviously arrays are not supported in VB6 like this so this is a nice extension to the language in TB which is not completely implemented i.e. events are not massaged to have index property. Apparently ETA on this feature getting fixed can only be shared by Wayne.
One way going forward from here is to implement event stubs in TB and forward these as function calls to a callback object in VB6, something like this
Code:
Private WithEvents m_oVoice As SpeechLib.SpVoice
Private m_oCallback As Object
Public Function CreateSpVoice(oCallback As Object) As Object
Set m_oVoice = New SpeechLib.SpVoice
Set m_oCallback = oCallback
Set CreateSpVoice = m_oVoice '--- fixed: setting retval was missing
End Function
Private Sub m_oVoice_StartStream(ByVal StreamNumber As Long, ByVal StreamPosition As Variant)
If Not m_oCallback Is Nothing Then
m_oCallback.SpVoice_StartStream StreamNumber, StreamPosition
End If
End Sub
Private Sub m_oVoice_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
If Not m_oCallback Is Nothing Then
m_oCallback.SpVoice_Word StreamNumber, StreamPosition, CharacterPosition, Length
End If
End Sub
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
Edit: I got it!!
Code:
Option Explicit
Private m_oProxy64 As Object
Private WithEvents m_Voice As SpVoice
Private Sub Form_Load()
Set m_oProxy64 = GetObject("MyApp.MyProxy")
Set m_Voice = m_oProxy64.CreateSPVoice(Me)
m_Voice.Speak "hi!"
End Sub
Public Sub SpVoice_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
Stop
End Sub
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
The Voice object can raise events only on the TB side and you forward them to the VB6 side via callbacks. You need to declare a Public Sub on the VB6 side with the name of each event you want to forward.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
I am stuck:
I get the error "A call for a property or method must not contain a reference to a private object - neither as an argument nor as a return value." in my scenario.
I guess I have to use "Implements" in VB6, but I can an error saying that I must implement "SpVoice_Word" which is the method that TB requires to send the class messages. When I did implement it, the error wouldn't go away, so I guess there is something else...
Here is my scenario:
I have a Form1:
Code:
Option Explicit
Private WithEvents m_Voices As cSPVoices
Private WithEvents m_Voice As SpVoice
Private m_oProxy64 As Object 'Currently not used
Private m_sName$
Private m_lCount&
Private Sub btnYetAnotherVoiceVoice_Click()
m_lCount = m_lCount + 1
If m_lCount = 1 Then
m_sName = "david"
ElseIf m_lCount = 2 Then
m_sName = "andreas"
ElseIf m_lCount = 3 Then
m_sName = "hedda"
End If
Dim nYetAnotherVoice As cSpVoice
Set nYetAnotherVoice = New cSpVoice
nYetAnotherVoice.CreateVoiceAndSelectSpeaker m_sName
nYetAnotherVoice.Speak "Hi!"
m_Voices.AddAVoiceToMyCollection_And_Set_Me_As_Callback nYetAnotherVoice
End Sub
Private Sub Form_Load()
Set m_Voices = New cSPVoices
End Sub
Code:
class cSpVoices:
Option Explicit
Implements cSpVoice
Public Event Word(ByVal CharacterPosition As Long, ByVal Length As Long)
Private m_MyVoices() As New cSpVoice
Public Sub AddAVoiceToMyCollection_And_Set_Me_As_Callback(ByRef u As cSpVoice)
Dim lUB&
lUB = UBound(m_MyVoices) + 1
ReDim Preserve m_MyVoices(0 To lUB)
Set m_MyVoices(lUB) = u
Set m_MyVoices(lUB).Callback(lUB) = Me
End Sub
Private Sub Class_Initialize()
ReDim m_MyVoices(0)
End Sub
Public Sub WordSpokenEvent(ByVal Index As Long, ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
RaiseEvent Word(CharacterPosition, Length)
End Sub
Private Sub cSpVoice_CreateVoiceAndSelectSpeaker(ByVal uVoiceName As String)
End Sub
Private Sub cSpVoice_SpVoice_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
End Sub
Private Sub cSpVoice_Speak(ByVal u As String)
End Sub
class cSpVoice:
Code:
Option Explicit
Private m_oProxy64 As Object
Private WithEvents m_Voice As SpVoice
Private m_Callback As cSPVoices
Private m_Index As Long
Public Sub CreateVoiceAndSelectSpeaker(ByVal uVoiceName As String)
Set m_oProxy64 = GetObject("MyApp.MyProxy")
Set m_Voice = m_oProxy64.CreateSPVoice(Me)
pSetSpeaker uVoiceName
End Sub
Friend Property Get Callback(Optional ByVal Index As Long) As cSpVoice
Set Callback = m_Callback
End Property
Friend Property Set Callback(ByVal Index As Long, ByRef RHS As cSPVoices)
m_Index = Index
Set m_Callback = RHS
End Property
Public Sub SpVoice_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
If m_Callback Is Nothing Then
Exit Sub
End If
m_Callback.WordSpokenEvent m_Index, StreamNumber, StreamPosition, CharacterPosition, Length
End Sub
Public Sub Speak(ByVal u As String)
m_Voice.Speak u
End Sub
Private Function pSetSpeaker(ByVal uVoiceNameAsGUID As String) As Boolean
On Error GoTo ErrHandler
Dim Token As SpeechLib.SpObjectToken
Set m_Voice = New SpVoice
For Each Token In m_Voice.GetVoices
Dim sName$
sName = Token.GetAttribute("Name")
Dim l&
l = InStr(1, sName, uVoiceNameAsGUID, vbTextCompare)
If l > 0 Then
Dim sConcName$
sConcName = "Name=" & sName
Dim sConcLanguage$
sConcLanguage = "Language=" & Token.GetAttribute("Language")
Set m_Voice.Voice = m_Voice.GetVoices(sConcName, sConcLanguage).Item(0)
Exit Function
End If
Next Token
Exit Function
ErrHandler:
Debug.Print Err.Description
Debug.Assert False
End Function
I have attached my project.
Last edited by tmighty2; Aug 7th, 2024 at 08:54 PM.
Re: Compatibility of 32-bit VB6 Applications with 64-bit SAPI
I used the callback mechanism in a class like this in the past without problems.
I don't see the difference between a class and a form.
You say that I should make a new instance of a form for each voice, right? Isn't that just like a class?