Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a higher-level abstraction over IDispatch #229

Closed
Keithcat1 opened this issue May 11, 2021 · 11 comments
Closed

Provide a higher-level abstraction over IDispatch #229

Keithcat1 opened this issue May 11, 2021 · 11 comments
Labels
enhancement New feature or request help wanted Good candidate for others to work on

Comments

@Keithcat1
Copy link

One day I had a problem, wanting to use SAPI in my application via COM. Now I have two problems.
It would be nice if there was an extension on IDispatch which had some functions that accepted a List or something as arguments and automatically converted them to COM types and called the specified method or set the specified property.
I saw the example/idispatch.dart wrapper class, but that doesn't seem to compile anymore and calling a method is still pretty complicated.

@timsneath
Copy link
Contributor

The idispatch.dart example should work, at least if you're on today's stable builds. (We're investigating a potential regression in the latest dev builds: see dart-lang/sdk#46004). If it doesn't work, I'd love any more information you can provide.

You're right -- it's a very low level way of making a COM call, and could benefit from some abstraction. I don't know that I'm going to have time to do this any time soon, but keeping the issue open and retitling it so that others can offer help.

@timsneath timsneath changed the title Make using COM easier Provide a higher-level abstraction over IDispatch May 19, 2021
@timsneath timsneath added enhancement New feature or request help wanted Good candidate for others to work on labels May 19, 2021
@mabDc
Copy link

mabDc commented Mar 16, 2022

i had found the way to tts (text to speech) using sapi with code below by calling windowsSpeakAsync("something")
but i have problems in how to stop and get vtable index as // vtable begins at 3, is 4 entries long.

https://github.com/timsneath/win32/blob/0153760170fef9f3ef907f83bb11859faa5563cf/lib/src/com/IDispatch.dart#L26-L30

import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';

const CLSID_SpVoice = '{96749377-3391-11D2-9EE3-00C04F797396}';

const IID_ISpVoice = '{6C44DF74-72B9-4992-A1EC-EF996E0422D4}';

final ptr = calloc<COMObject>()..ref.genNew();
int _speak(
  Pointer<Utf16> pwcs,
  int dwFlags,
  int pulStreamNumber,
) =>
    Pointer<
            NativeFunction<
                Int32 Function(
      Pointer obj,
      Pointer<Utf16> pwcs,
      Uint32 dwFlags,
      Uint32 pulStreamNumber,
    )>>.fromAddress(ptr.ref.vtable.elementAt(20).value)
        .asFunction<
            int Function(
      Pointer obj,
      Pointer<Utf16> pwcs,
      int dwFlags,
      int pulStreamNumber,
    )>()(ptr.ref.lpVtbl, pwcs, dwFlags, pulStreamNumber);

int windowsSpeak(String s) {
  // Initialize COM
  var hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
  if (FAILED(hr)) {
    throw "WindowsException(hr)";
  }

  hr = CoCreateInstance(
    GUIDFromString(CLSID_SpVoice),
    nullptr,
    CLSCTX_ALL,
    GUIDFromString(IID_ISpVoice),
    ptr,
  );

  var i = _speak(s.toNativeUtf16(), 0, NULL);
  if (FAILED(hr)) throw "失败";
  CoUninitialize();
  return i;
}

Future<int> windowsSpeakAsync(String s) {
  return compute(windowsSpeak, s);
}

const COINIT_APARTMENTTHREADED = 0x2;

const NULL = 0;

/// @nodoc
const CLSCTX_INPROC_SERVER = 0x1;

/// @nodoc
const CLSCTX_INPROC_HANDLER = 0x2;

/// @nodoc
const CLSCTX_LOCAL_SERVER = 0x4;

/// @nodoc
const CLSCTX_INPROC_SERVER16 = 0x8;

/// @nodoc
const CLSCTX_REMOTE_SERVER = 0x10;

/// @nodoc
const CLSCTX_ALL = CLSCTX_INPROC_SERVER |
    CLSCTX_INPROC_HANDLER |
    CLSCTX_LOCAL_SERVER |
    CLSCTX_REMOTE_SERVER;

// *** COM STRUCTS ***

// typedef struct _GUID {
//     unsigned long  Data1;
//     unsigned short Data2;
//     unsigned short Data3;
//     unsigned char  Data4[ 8 ];
// } GUID;

/// Represents a globally unique identifier (GUID).
///
/// {@category Struct}
class GUID extends Struct {
  @Uint32()
  int Data1;
  @Uint16()
  int Data2;
  @Uint16()
  int Data3;
  @Uint64()
  int Data4;

  factory GUID.allocate() => calloc<GUID>().ref
    ..Data1 = 0
    ..Data2 = 0
    ..Data3 = 0
    ..Data4 = 0;

  /// Create GUID from common {FDD39AD0-238F-46AF-ADB4-6C85480369C7} format
  factory GUID.fromString(String guidString) {
    assert(guidString.length == 38);
    final guid = calloc<GUID>().ref;
    guid.Data1 = int.parse(guidString.substring(1, 9), radix: 16);
    guid.Data2 = int.parse(guidString.substring(10, 14), radix: 16);
    guid.Data3 = int.parse(guidString.substring(15, 19), radix: 16);

    // final component is pushed on the stack in reverse order per x64
    // calling convention. This is a funky workaround until FFI supports
    // passing structs by value.
    final rawString = guidString.substring(35, 37) +
        guidString.substring(33, 35) +
        guidString.substring(31, 33) +
        guidString.substring(29, 31) +
        guidString.substring(27, 29) +
        guidString.substring(25, 27) +
        guidString.substring(22, 24) +
        guidString.substring(20, 22);

    // We need to split this to avoid overflowing a signed int.parse()
    guid.Data4 = (int.parse(rawString.substring(0, 4), radix: 16) << 48) +
        int.parse(rawString.substring(4, 16), radix: 16);

    return guid;
  }

  /// Print GUID in common {FDD39AD0-238F-46AF-ADB4-6C85480369C7} format
  @override
  String toString() {
    final comp1 = (Data4 & 0xFF).toRadixString(16).padLeft(2, '0') +
        ((Data4 & 0xFF00) >> 8).toRadixString(16).padLeft(2, '0');

    // This is hacky as all get-out :)
    final comp2 = ((Data4 & 0xFF0000) >> 16).toRadixString(16).padLeft(2, '0') +
        ((Data4 & 0xFF000000) >> 24).toRadixString(16).padLeft(2, '0') +
        ((Data4 & 0xFF00000000) >> 32).toRadixString(16).padLeft(2, '0') +
        ((Data4 & 0xFF0000000000) >> 40).toRadixString(16).padLeft(2, '0') +
        ((Data4 & 0xFF000000000000) >> 48).toRadixString(16).padLeft(2, '0') +
        (BigInt.from(Data4 & 0xFF00000000000000).toUnsigned(64) >> 56)
            .toRadixString(16)
            .padLeft(2, '0');

    return '{${Data1.toRadixString(16).padLeft(8, '0').toUpperCase()}-'
        '${Data2.toRadixString(16).padLeft(4, '0').toUpperCase()}-'
        '${Data3.toRadixString(16).padLeft(4, '0').toUpperCase()}-'
        '${comp1.toUpperCase()}-'
        '${comp2.toUpperCase()}}';
  }

  /// Create GUID from common {FDD39AD0-238F-46AF-ADB4-6C85480369C7} format
  void setGUID(String guidString) {
    assert(guidString.length == 38);
    Data1 = int.parse(guidString.substring(1, 9), radix: 16);
    Data2 = int.parse(guidString.substring(10, 14), radix: 16);
    Data3 = int.parse(guidString.substring(15, 19), radix: 16);

    // Final component is pushed on the stack in reverse order per x64
    // calling convention.
    final rawString = guidString.substring(35, 37) +
        guidString.substring(33, 35) +
        guidString.substring(31, 33) +
        guidString.substring(29, 31) +
        guidString.substring(27, 29) +
        guidString.substring(25, 27) +
        guidString.substring(22, 24) +
        guidString.substring(20, 22);

    // We need to split this to avoid overflowing a signed int.parse()
    Data4 = (int.parse(rawString.substring(0, 4), radix: 16) << 48) +
        int.parse(rawString.substring(4, 16), radix: 16);
  }
}

Pointer<GUID> GUIDFromString(String guid) => calloc<GUID>()..ref.setGUID(guid);

/// A representation of a generic COM object. All Dart COM objects inherit from
/// this class.
///
/// {@category com}
class COMObject extends Struct {
  Pointer<IntPtr> lpVtbl;

  Pointer<IntPtr> get vtable => Pointer.fromAddress(lpVtbl.value);

  genNew() {
    lpVtbl = calloc<IntPtr>();
  }
}

final _ole32 = DynamicLibrary.open('ole32.dll');

final CoInitializeEx = _ole32.lookupFunction<
    Int32 Function(Pointer<Void> pvReserved, Uint32 dwCoInit),
    int Function(Pointer<Void> pvReserved, int dwCoInit)>('CoInitializeEx');

/// Creates a single uninitialized object of the class associated with a
/// specified CLSID. Call CoCreateInstance when you want to create only one
/// object on the local system. To create a single object on a remote
/// system, call the CoCreateInstanceEx function. To create multiple
/// objects based on a single CLSID, call the CoGetClassObject function.
///
/// ```c
/// HRESULT CoCreateInstance(
///   REFCLSID  rclsid,
///   LPUNKNOWN pUnkOuter,
///   DWORD     dwClsContext,
///   REFIID    riid,
///   LPVOID    *ppv
/// );
/// ```
/// {@category ole32}
final CoCreateInstance = _ole32.lookupFunction<
    Int32 Function(Pointer<GUID> rclsid, Pointer<IntPtr> pUnkOuter, Uint32 dwClsContext,
        Pointer<GUID> riid, Pointer<COMObject> ppv),
    int Function(Pointer<GUID> rclsid, Pointer<IntPtr> pUnkOuter, int dwClsContext,
        Pointer<GUID> riid, Pointer<COMObject> ppv)>('CoCreateInstance');

/// Closes the COM library on the current thread, unloads all DLLs loaded
/// by the thread, frees any other resources that the thread maintains, and
/// forces all RPC connections on the thread to close.
///
/// ```c
/// void CoUninitialize();
/// ```
/// {@category ole32}
final CoUninitialize =
    _ole32.lookupFunction<Void Function(), void Function()>('CoUninitialize');

// #define FAILED(hr) (((HRESULT)(hr)) < 0)
bool FAILED(int result) => result < 0;

@mabDc
Copy link

mabDc commented Mar 16, 2022

okay, i found it is the same order from sapi51.h and below code works fine.

// from sapi51.h
//     MIDL_INTERFACE("6C44DF74-72B9-4992-A1EC-EF996E0422D4")
//     ISpVoice : public ISpEventSource
//     {
//     public:
//         virtual HRESULT STDMETHODCALLTYPE SetOutput(
//             /* [in] */ IUnknown *pUnkOutput,
//             /* [in] */ BOOL fAllowFormatChanges) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetOutputObjectToken(
//             /* [out][annotation] */
//             _Outptr_  ISpObjectToken **ppObjectToken) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetOutputStream(
//             /* [out] */ ISpStreamFormat **ppStream) = 0;

//         virtual HRESULT STDMETHODCALLTYPE Pause( void) = 0;

//         virtual HRESULT STDMETHODCALLTYPE Resume( void) = 0;

//         virtual HRESULT STDMETHODCALLTYPE SetVoice(
//             /* [in] */ ISpObjectToken *pToken) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetVoice(
//             /* [out][annotation] */
//             _Outptr_  ISpObjectToken **ppToken) = 0;

//         virtual HRESULT STDMETHODCALLTYPE Speak(
//             /* [string][in][annotation] */
//             _In_opt_  LPCWSTR pwcs,
//             /* [in] */ DWORD dwFlags,
//             /* [out][annotation] */
//             _Out_opt_  ULONG *pulStreamNumber) = 0;

//         virtual HRESULT STDMETHODCALLTYPE SpeakStream(
//             /* [in] */ IStream *pStream,
//             /* [in] */ DWORD dwFlags,
//             /* [out][annotation] */
//             _Out_opt_  ULONG *pulStreamNumber) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetStatus(
//             /* [out] */ SPVOICESTATUS *pStatus,
//             /* [out][annotation] */
//             _Outptr_  LPWSTR *ppszLastBookmark) = 0;

//         virtual HRESULT STDMETHODCALLTYPE Skip(
//             /* [string][in] */ LPCWSTR pItemType,
//             /* [in] */ long lNumItems,
//             /* [out] */ ULONG *pulNumSkipped) = 0;

//         virtual HRESULT STDMETHODCALLTYPE SetPriority(
//             /* [in] */ SPVPRIORITY ePriority) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetPriority(
//             /* [out] */ SPVPRIORITY *pePriority) = 0;

//         virtual HRESULT STDMETHODCALLTYPE SetAlertBoundary(
//             /* [in] */ SPEVENTENUM eBoundary) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetAlertBoundary(
//             /* [out] */ SPEVENTENUM *peBoundary) = 0;

//         virtual HRESULT STDMETHODCALLTYPE SetRate(
//             /* [in] */ long RateAdjust) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetRate(
//             /* [out] */ long *pRateAdjust) = 0;

//         virtual HRESULT STDMETHODCALLTYPE SetVolume(
//             /* [in] */ USHORT usVolume) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetVolume(
//             /* [out] */ USHORT *pusVolume) = 0;

//         virtual HRESULT STDMETHODCALLTYPE WaitUntilDone(
//             /* [in] */ ULONG msTimeout) = 0;

//         virtual HRESULT STDMETHODCALLTYPE SetSyncSpeakTimeout(
//             /* [in] */ ULONG msTimeout) = 0;

//         virtual HRESULT STDMETHODCALLTYPE GetSyncSpeakTimeout(
//             /* [out] */ ULONG *pmsTimeout) = 0;

//         virtual /* [local] */ HANDLE STDMETHODCALLTYPE SpeakCompleteEvent( void) = 0;

//         virtual /* [local] */ HRESULT STDMETHODCALLTYPE IsUISupported(
//             /* [in] */ LPCWSTR pszTypeOfUI,
//             /* [in] */ void *pvExtraData,
//             /* [in] */ ULONG cbExtraData,
//             /* [out] */ BOOL *pfSupported) = 0;

//         virtual /* [local] */ HRESULT STDMETHODCALLTYPE DisplayUI(
//             /* [in] */ HWND hwndParent,
//             /* [in] */ LPCWSTR pszTitle,
//             /* [in] */ LPCWSTR pszTypeOfUI,
//             /* [in] */ void *pvExtraData,
//             /* [in] */ ULONG cbExtraData) = 0;

//     };

import 'dart:ffi';

import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';

class ISapi extends IUnknown {
// vtable begins at 13, is 30 entries long.
  ISapi(Pointer<COMObject> ptr) : super(ptr);

  int Pause() => Pointer<
              NativeFunction<
                  Int32 Function(
        Pointer obj,
      )>>.fromAddress(ptr.ref.vtable.elementAt(16).value)
          .asFunction<
              int Function(
        Pointer obj,
      )>()(ptr.ref.lpVtbl);

  int Resume() => Pointer<
              NativeFunction<
                  Int32 Function(
        Pointer obj,
      )>>.fromAddress(ptr.ref.vtable.elementAt(17).value)
          .asFunction<
              int Function(
        Pointer obj,
      )>()(ptr.ref.lpVtbl);

  int Speak(
    Pointer<Utf16> pwcs,
    int dwFlags,
    int pulStreamNumber,
  ) =>
      Pointer<
              NativeFunction<
                  Int32 Function(
        Pointer obj,
        Pointer<Utf16> pwcs,
        Uint32 dwFlags,
        Uint32 pulStreamNumber,
      )>>.fromAddress(ptr.ref.vtable.elementAt(20).value)
          .asFunction<
              int Function(
        Pointer obj,
        Pointer<Utf16> pwcs,
        int dwFlags,
        int pulStreamNumber,
      )>()(ptr.ref.lpVtbl, pwcs, dwFlags, pulStreamNumber);
}

const CLSID_SpVoice = '{96749377-3391-11D2-9EE3-00C04F797396}';

const IID_ISpVoice = '{6C44DF74-72B9-4992-A1EC-EF996E0422D4}';

class Sapi {
  final ISapi iSapi;
  Sapi(this.iSapi);

  factory Sapi.createInstance() {
    final ptr = COMObject.createFromID(CLSID_SpVoice, IID_ISpVoice);
    final iSapi = ISapi(ptr);
    return Sapi(iSapi);
  }

  final SVSFlagsAsync = 1;
  final SVSFPurgeBeforeSpeak = 2;

  int speak(String s) {
    final pwcs = s.toNativeUtf16();
    try {
      final hr = iSapi.Speak(pwcs, SVSFlagsAsync | SVSFPurgeBeforeSpeak, NULL);
      if (FAILED(hr)) {
        throw WindowsException(hr);
      } else {
        final hr = iSapi.Resume();
        if (FAILED(hr)) {
          throw WindowsException(hr);
        } else {
          return hr;
        }
      }
    } finally {
      free(pwcs);
    }
  }

  int pause() {
    try {
      final hr = iSapi.Pause();
      if (FAILED(hr)) {
        throw WindowsException(hr);
      } else {
        return hr;
      }
    } finally {}
  }

  int resume() {
    try {
      final hr = iSapi.Resume();
      if (FAILED(hr)) {
        throw WindowsException(hr);
      } else {
        return hr;
      }
    } finally {}
  }

  void dispose() {
    free(iSapi.ptr);
    CoUninitialize();
  }
}

@timsneath
Copy link
Contributor

timsneath commented Mar 20, 2022

@mabDc I've added the speech client APIs directly to win32 (#379), so now you don't have to handcraft these COM interfaces.

This should greatly simplify your code. Here's a very straightforward example:

import 'dart:ffi';

import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';

const textToSpeak =
    'Dart is a portable, high-performance language from Google.';

void main() {
  CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
  final speechEngine = SpVoice.createInstance();
  final pText = textToSpeak.toNativeUtf16();
  speechEngine.Speak(pText, SPEAKFLAGS.SPF_IS_NOT_XML, nullptr);
  free(pText);
  CoUninitialize();
}

@mabDc
Copy link

mabDc commented Mar 20, 2022

nice. thanks.

@Keithcat1
Copy link
Author

Keithcat1 commented Mar 23, 2022

What have you done! COM is supposed to be all painful and headache-inducing and you should have to look at a piece of code for half an hour before figuring out what it does. It's not supposed to be mostly readable.
Anyway, you managed to add this pretty much when I was regaining interest and was about to ask for it. Thanks!
Oh and what's that nullptr for? I thought that Speak only took 2 parameters?

@timsneath
Copy link
Contributor

Thank you for the kind comments, @Keithcat1!

The MSDN documentation is kinda messed up, but it looks like the third parameter is the stream number (for when you have multiple simultaneous streams): https://docs.microsoft.com/en-us/previous-versions/office/developer/speech-technologies/jj149384(v=msdn.10)

@mabDc
Copy link

mabDc commented Mar 27, 2022

What have you done! COM is supposed to be all painful and headache-inducing and you should have to look at a piece of code for half an hour before figuring out what it does. It's not supposed to be mostly readable. Anyway, you managed to add this pretty much when I was regaining interest and was about to ask for it. Thanks! Oh and what's that nullptr for? I thought that Speak only took 2 parameters?

@Keithcat1 u need look at sapi51.h for some information as i ve mentioned

    MIDL_INTERFACE("6C44DF74-72B9-4992-A1EC-EF996E0422D4")
    ISpVoice : public ISpEventSource
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE SetOutput(
            /* [in] */ IUnknown *pUnkOutput,
            /* [in] */ BOOL fAllowFormatChanges) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetOutputObjectToken(
            /* [out][annotation] */
            _Outptr_  ISpObjectToken **ppObjectToken) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetOutputStream(
            /* [out] */ ISpStreamFormat **ppStream) = 0;

        virtual HRESULT STDMETHODCALLTYPE Pause( void) = 0;

        virtual HRESULT STDMETHODCALLTYPE Resume( void) = 0;

        virtual HRESULT STDMETHODCALLTYPE SetVoice(
            /* [in] */ ISpObjectToken *pToken) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetVoice(
            /* [out][annotation] */
            _Outptr_  ISpObjectToken **ppToken) = 0;

        virtual HRESULT STDMETHODCALLTYPE Speak(
            /* [string][in][annotation] */
            _In_opt_  LPCWSTR pwcs,
            /* [in] */ DWORD dwFlags,
            /* [out][annotation] */
            _Out_opt_  ULONG *pulStreamNumber) = 0;

        virtual HRESULT STDMETHODCALLTYPE SpeakStream(
            /* [in] */ IStream *pStream,
            /* [in] */ DWORD dwFlags,
            /* [out][annotation] */
            _Out_opt_  ULONG *pulStreamNumber) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetStatus(
            /* [out] */ SPVOICESTATUS *pStatus,
            /* [out][annotation] */
            _Outptr_  LPWSTR *ppszLastBookmark) = 0;

        virtual HRESULT STDMETHODCALLTYPE Skip(
            /* [string][in] */ LPCWSTR pItemType,
            /* [in] */ long lNumItems,
            /* [out] */ ULONG *pulNumSkipped) = 0;

        virtual HRESULT STDMETHODCALLTYPE SetPriority(
            /* [in] */ SPVPRIORITY ePriority) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetPriority(
            /* [out] */ SPVPRIORITY *pePriority) = 0;

        virtual HRESULT STDMETHODCALLTYPE SetAlertBoundary(
            /* [in] */ SPEVENTENUM eBoundary) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetAlertBoundary(
            /* [out] */ SPEVENTENUM *peBoundary) = 0;

        virtual HRESULT STDMETHODCALLTYPE SetRate(
            /* [in] */ long RateAdjust) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetRate(
            /* [out] */ long *pRateAdjust) = 0;

        virtual HRESULT STDMETHODCALLTYPE SetVolume(
            /* [in] */ USHORT usVolume) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetVolume(
            /* [out] */ USHORT *pusVolume) = 0;

        virtual HRESULT STDMETHODCALLTYPE WaitUntilDone(
            /* [in] */ ULONG msTimeout) = 0;

        virtual HRESULT STDMETHODCALLTYPE SetSyncSpeakTimeout(
            /* [in] */ ULONG msTimeout) = 0;

        virtual HRESULT STDMETHODCALLTYPE GetSyncSpeakTimeout(
            /* [out] */ ULONG *pmsTimeout) = 0;

        virtual /* [local] */ HANDLE STDMETHODCALLTYPE SpeakCompleteEvent( void) = 0;

        virtual /* [local] */ HRESULT STDMETHODCALLTYPE IsUISupported(
            /* [in] */ LPCWSTR pszTypeOfUI,
            /* [in] */ void *pvExtraData,
            /* [in] */ ULONG cbExtraData,
            /* [out] */ BOOL *pfSupported) = 0;

        virtual /* [local] */ HRESULT STDMETHODCALLTYPE DisplayUI(
            /* [in] */ HWND hwndParent,
            /* [in] */ LPCWSTR pszTitle,
            /* [in] */ LPCWSTR pszTypeOfUI,
            /* [in] */ void *pvExtraData,
            /* [in] */ ULONG cbExtraData) = 0;

    };

@Keithcat1
Copy link
Author

How can I get an error message string from a HRESULT error code?
Also, the GetVoices method is missing. Apparently the ISpeechObjectToken interface, I think it's called. I was planning to use it

@timsneath
Copy link
Contributor

I've added ISpeechObjectToken (and ISpeechObjectTokens, which I think is the one you really want) in 78d7e12.

If you throw WindowsException(hr), an HRESULT will be displayed with a friendly message. You can also check the code of WindowsException to see how to format an HRESULT as a string using Win32 APIs.

@Keithcat1
Copy link
Author

Didn't know that. I'll throw a WindowsException if the HRESULT returned from a call to a SAPI function is not 0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Good candidate for others to work on
Projects
None yet
Development

No branches or pull requests

3 participants