Thank you! It seems to work, but I don't understand why it works for a form but not a class.
Printable View
Thank you! It seems to work, but I don't understand why it works for a form but not a class.
Yeah usually you would be right, a form is indeed a class, but for the purpose of inter-process communication using the RunningObjectTable you can only use forms. Apparently there are some subtle differences between a form and a class in this regard. I remember there was a discussion about this in an older thread (can't be bothered to search right now) and there was someone who figured out the nitty-gritty details and came up with a dirty hack that allowed classes to be used in this manner as well.
This has been an incredible experience. Thank you so much for recommending TB and for the lessons about callbacks and what com can do.
Thank you!!!
How do you start such a TB "server"? Do you have some best practices to share? My idea would be to try to find it by its window text, and if not found, start it. I am not sure how "crash safe" my approach is. Or if it would perhaps even be necessary to kill the server if something goes wrong.. If anybody has any experience with this and tipps, please let me know.
If "GetObject" fails then the server is not started, pretty straightforward.
@tmighty2: Kudos for coming up with cSpVoice wrapper. This would have been my next suggestion -- a class which accepts a callback object and an index so it can forward original SpeechLib.SpVoice events on functions of the callback by prefixing their parameters with the index.
This is what VBx does with control arrays -- it generates both the control array class and a hidden wrapper class which calls back to the control array class so it can raise events with indexes.
cheers,
</wqw>
How could I get the Form?
I have tried anything I could imagine.
In the tb form I have tried Function, Property and GetMe (in order to return the form itself as on object).
For example:
And on the VB6 side in a Form:Code:Private m_oCallback As Object
Private m_oCallbackMe As Object
Public Sub ShutDown()
Unload Me
End Sub
Public Property Get AboutYou2() As String
AboutYou2 = "About me 2"
End Property
Public Function AboutYou1() As String
AboutYou1 = "about me 1"
End Function
Public Sub ShowYourself()
Me.Visible = True
End Sub
Public Sub HideYourself()
Me.Visible = False
End Sub
Public Function GetMe(oCallback As Object) As Object
Set m_oCallbackMe = oCallback
Set GetMe = Me
End Function
But I was not able to talk to the Form.Code:Set m_oProxy64 = GetObject("myidentifier")
If m_oProxy64 Is Nothing Then
Debug.Assert False
End If
Set m_ProxyForm = m_oProxy64.GetMe(Me)
Perhaps I am missing something.
Can you help?
"m_oProxy64" is the TB form, provided you used the "Me" keyword in the "PutObject" function. You can even access other controls on the form: m_oProxy64.Command1.Caption = "New Caption".
The discussion was about VB6 forms and private VB6 classes not being able to be placed in ROT for no apparent reason (needed a "cure" in current VBProject structs). There is no such restriction in TB so regular classes get put in ROT too.
Here is a somewhat full sample using TB and VB6: Moniker2.zip
Just make sure to build x64 target of Moniker.twinproj before running the VB6 project. It will search and auto-start (hidden) Moniker_win64.exe in exact location. It will make sure to shutdown (hidden) proxy process on form unload.
Here is the complete VB6 source code
Another better approach would be to pass "MyApp.MyProxy" moniker filename on command-line so that a UUID can be generated by client code and used as "communication channel name". If you look at chome.exe processes' command-lines these are littered with such on-the-fly generated UUIDs.Code:Option Explicit
Private m_oProxy As Object
Private m_oWrapper As Object
Private Function GetMyProxy() As Object
Const MONIKER_WIN64_EXE As String = "..\TB\Build\Moniker_win64.exe"
On Error GoTo EH
If m_oProxy Is Nothing Then
Set m_oProxy = GetObject("MyApp.MyProxy")
End If
Set GetMyProxy = m_oProxy
Exit Function
EH:
Shell App.Path & "\" & MONIKER_WIN64_EXE & " /hidden"
Set m_oProxy = GetObject("MyApp.MyProxy")
Set GetMyProxy = m_oProxy
End Function
Private Sub Form_Click()
On Error GoTo EH
If m_oWrapper Is Nothing Then
Set m_oWrapper = GetMyProxy.CreateSpVoice(Me)
End If
m_oWrapper.SpVoice.Speak "this is a test", 1
Exit Sub
EH:
MsgBox Err.Description, vbCritical
Set m_oWrapper = Nothing
Set m_oProxy = Nothing
End Sub
Private Sub Form_Unload(Cancel As Integer)
If Not m_oProxy Is Nothing Then
m_oProxy.Shutdown
End If
End Sub
Public Sub SpVoice_StartStream(ByVal StreamNumber As Long, ByVal StreamPosition As Variant)
Print "StartStream, StreamNumber=" & StreamNumber & ", StreamPosition=" & StreamPosition
End Sub
Public Sub SpVoice_Word(ByVal StreamNumber As Long, ByVal StreamPosition As Variant, ByVal CharacterPosition As Long, ByVal Length As Long)
Print "StartStream, Word=" & StreamNumber & ", StreamPosition=" & StreamPosition & ", CharacterPosition=" & CharacterPosition & ", Length=" & Length
End Sub
Edit: Just updated Moniker2.zip above with this idea about random UUID and included x64 TB executable.
cheers,
</wqw>
Since "Shell" returns immediately and doesn't wait for the external program to actually finish loading, isn't there a possibility the subsequent "GetObject" will fail?Code:EH:
Shell App.Path & "\" & MONIKER_WIN64_EXE & " /hidden"
Set m_oProxy = GetObject("MyApp.MyProxy")
> Since "Shell" returns immediately and doesn't wait for the external program to actually finish loading, isn't there a possibility the subsequent "GetObject" will fail?
That's what I though and was prepared to use ShellExecute with SEE_MASK_NOCLOSEPROCESS and WaitForInputIdle on returned hProcess i.e. the usual dance with a lot of API declares but it works with built-in Shell just fine here which is not something I expected to get away so easily.
cheers,
</wqw>
Thank you to The trick for providing detailed step by step. I just found this note that says Microsoft has stopped reading from HKEY_CLASSES_ROOT\AppID since Windows 2003, and uses the one in HKLM only at HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID. Perhaps that's why the OP was having issues.
https://learn.microsoft.com/en-us/wi...in32/com/runas
If you need several voices with their events at the same time, would be recommend 1 "Moniker_win64.exe" for all voices or 1 "Moniker_win64.exe" for each voice?
That’s an interesting question. With multiple out-of-process servers you can get per process multi-tasking “for free” but the COM object in particular already uses threading to multi-task in any single process so the benefits of spawning multiple EXEs in dubious or non-existant.
Do you face any blocking issues with single spawn approach?
No, I don't experience anything strange.
(Edit: No more question, all is clear now. Thank you! Great enhancement!)
I terminate my app often using the stop button in the VB6 IDE.
This leads to several moniker exes being left open.
Is there any recommend way to check if the process that the callback belongs to is still alive?
I could think of using a timer to check if window title (the process that the callback object belongs to could open an invisible window with a UUID as the caption) is found.
Or trying to find the process from time to time.
Is there any recommended which of checking if the process is still alive? To be clear: The TB exe should check if the other process (in my case the VB6 app) is still running. If not, the TB exe should close.
You should approach this issue the other way around, if a TB exe is still running then you should reuse it instead of opening a new one (as I suggested in post #45).
When running in IDE you can use a fixed GUID for the moniker name so that any outstanding/running TB server will respond to GetObject(FIXED_MONIKER) no matter if IDE was restarted in the mean time.
Note that on stopping debug session all references to external COM objects are cleared so proxy/stub wrappers in TB process are cleared ok in this case too.
How to call the 32-bit ActiveX DLL? In the 64-bit program in VC + +?
The solution (at least as by suggestion of chatgpt and Google gemini) is to use a Heartbeat which will be sent by the calling VB6 application to the TB app from time to time to indicate that it it still alive.
I have attached my TB to illustrate that just in case somebody requires to see that.
TB is really easy to work with and works fine so far. I really appreciate getting to know it due to this problem that I had. I use it paying regular monthly payment.
Thank you!
In your latest demo that you posted here, are you not using CreateObject anymore?
Code:Public Function CreateObject(sProgID As String) As Object
Set CreateObject = VBA.CreateObject(sProgID)
End Function
I suspected that this would be the error that I experience on some customers computer, but the error occurs here:
Does you have any idea why this might happen?Code:Private m_sProxyName_Aka_UUID$
Private m_oProxy64 As Object
I start it like this:
Dim sCmd$
sCmd="D:\Dev\Projects\tws64\TB\Build\twsMoniker64_win64.exe 2e1316f8-a36a-4734-9db6-cc41a7696dd2"
Dim d As Double
d = Shell(sCmd)
Set m_oProxy64 = GetObject(m_sProxyName_Aka_UUID)
And it throws an error in that last line "GetObject" with "Automation error. Invalid syntax" , err.number: -2147221020
"Invalid syntax" is raised when the moniker name is not recognized. Probably should retry GetObject call (with a timeout) untill Shell completes spawning the worker process and server object is registered in ROT.
> In your latest demo that you posted here, are you not using CreateObject anymore?
This is just a sample cheap and useful method you can use to "spawn" new server objects inside the x64 process. Can't remember if it's there for any other purpose.
cheers,
</wqw>
I tried it 10 times with a pause of 1 second between each attempt, so 10 seconds altogether. That should be enough, right?
It works fine on my computer but fails on a clients computer running Windows 11 Home, Version 10.0.22631 Build 22631.
Do you have any recommendation what I should check next?
I have attached the Twinbasic project.
Please ignore the childish over-explaining variable names. I had to make some sense of it, and doing that helps me.
Edit: I have now put vb6 and tb into a single folder and attached it as spvoiceproxy2.zip.
- Edit: I have put all files together in a single zip in my previous post. -
I am not familiar with this error at all and currently stuck. Would it help to shrink the project? I am not sure if it's understandable.
My code works fine on my computer, and honestly I got no clue what else to test.
One thing I noticed when opening SpVoiceMonikerTest.vbp is that Microsoft Speech Object Library reference is MISSING on my machine.
Apparently you are referencing some incompatible (newer or older) version on the DLL. Once I fixed this the project seems to work in the IDE (while compiled SpVoiceMonikerTest.exe is something old, not sure what to test here).
Could it be your clients PCs have different Microsoft Speech installed? I have no previous experience with this library, first time seeing it, so cannot tell if this is normal or if you should use late-bound calls similar to dealing with MS Office incompatibilities.
Anyway, cannot come up with anything obviously wrong with your code on first glace.
cheers,
</wqw>
It is a file that should be present on all Windows machines from Windows 10 upwards as it is used as a standard feature:
Users who can not see and therefore use a computer voice to have the text on the screen spoken to them, use computer voices, and they are controlled via SAPI.
SAPI.dll must exist as both 32 bit and 64 bit version on the computer.
Users can not replace this SAPI.dll easily. It is not a file that users are expected to update themselves.
Do you have any idea how to solve them when you say that on your machine it was missing?
May I ask you where your was located and which version, etc.?
Or is this TwinBasic related?
Do I understand it correctly that you say I have to late binding?
If yes then I would not be able to have the SpVoice fire events, correct?
This is my current code in TwinBasic:
This would not work if I used "As Object", correct?Code:Public WithEvents AnSpVoice As SpeechLib.SpVoice
ps: I posted the reference that I used in TwinBasic to chatgpt, and it said that the library that I'm using, identified by the GUID "C866CA3A-32F7-11D2-9602-00C04F8EE628", is part of Windows' built-in speech capabilities, specifically tied to the Speech API.(SAPI).
This is what I see when I double click the reference in TwinBasic:
I checked the customer's registry, and it is found:Code:[ LibraryId ("C866CA3A-32F7-11D2-9602-00C04F8EE628") ]
[ Version (5.4) ]
[ Description ("Microsoft Speech Object Library") ]
Library SpeechLib
' Original type library: C:\WINDOWS\System32\Speech\Common\sapi.dll
' NOTE: Offsets and lengths calculated for current Win64 target.
Attachment 193110Code:[HKEY_CLASSES_ROOT\CLSID\{0655E396-25D0-11D3-9C26-00C04F8EF87C}\TypeLib]
@="{C866CA3A-32F7-11D2-9602-00C04F8EE628}"
I see that the version of sapi.dll of the customer's computer is newer than mine, but SAPI exists since many years, and I find it highly unlikely that a change to it was introduced which in turn would cause an incompatibility.
It is possible to Raise Events from late-bound Objects.
This is the "fixed" reference in SpVoiceMonikerTest.vbp on my machine w/ win11
Originally what I found in the ZIP was this:Code:Reference=*\G{C866CA3A-32F7-11D2-9602-00C04F8EE628}#5.4#0#C:\WINDOWS\System32\Speech\Common\sapi.dll#Microsoft Speech Object Library
cheers,Code:Reference=*\G{D3C4A7F2-7D27-4332-B41F-593D71E16DB1}#b.0#0#..\..\..\..\Program Files (x86)\Common Files\Microsoft Shared\Speech\Platform\v11.0\mssps.dll#Microsoft Speech Object Library
</wqw>
When I import the customers sapi.dll instead of mine and check the TwinBasic References then, apart from the file location, the 2 files match.
I believe it's a TwinBasic problem.
Edit: Oh you are talking about the VB6 project!!
I have changed it now, but it doesn't make a difference.
When I use the voice inside the TwinBasic moniker / proxy. It works. It just seems that it is unable to pass it to my VB6 exe.
I got it.
In VB6, I can just declare it as
Private m_Voice As Object
The events are fired to the vb6 form, so I don't need them for the voice. :-)
Finally!!!!!!!!!
Full project is attached. I didnt have time to clean it up, but it works now.
Edit:
It's not working any more. Now it can't even get the proxy.
This is so frustrating, really.
Bullet dodged, until something else crumbles -- the epitome of "hope programming" :-)) Hope you find some time to cleanup/refactor this code.
cheers,
</wqw>
I had used it in a small test app, and it worked.
I use just the same file in my production app, and it can not even get the proxy.
My production app is signed and has a manifest.
When I run my app as an admin, it works.
It will immediately get the proxy without any 2nd or third attempt..
Why????????????? The proxy is in my app's path.
My app is signed and manifested. The proxy is not.
The proxy and my app are in Program Files (x86)\<appname>.
Why does my app need to have admin rights to interact with the proxy?
When I put the small test app (which is not signed and not manifested) into the same Program Files (x86)\<appname> directory, it does NOT require to be admin to be executed.
Edit: Hmmm, I see... since my app has uiAccess manifest, it can interact with the ui which is priviledged. Allowing my app to control a proxy which does not have the same privileges could be seen as bad as the proxy does something in its name, but in fact it's my app which does the possibly bad thing with its privileges. That is why the proxy must also have the manifest, I guess.
Edit2: I have given it the same manifest (except * for processorarchitecture instead of x86) and codesigned the moniker.
Now it can't be started using shell and throws invalid procedure or argument error when I use the usual Shell command to run it.