Powershell: Speak method appears to be broken in 7 and 7.1.0-preview1

Created on 1 Apr 2020  路  15Comments  路  Source: PowerShell/PowerShell

The following code works in PowerShell 5.1, but throws an error on 7.0 and 7.1.0-preview1

Add-Type -AssemblyName System.speech

$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
PS C:UsersUser> $speak.Speak("This is Windows talking.")

On PowerShell 5.1, the expected result happens, i. e. the system speaks the text. On 7.0 and apparently later, it throws the following error:
MethodInvocationException: Exception calling "Speak" with "1" argument(s): "Object reference not set to an instance of an object."

Issue-Question Waiting - DotNetCore

Most helpful comment

System.Speech uses reflection to access the non-public field RegistryKey.hkey which was renamed in core to _hkey. Maybe worth opening an issue on dotnet/runtime to inform them that there's a Microsoft assembly depending on that field name.

All 15 comments

I get this:

PS C:\foo> $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
New-Object: Cannot find type [System.Speech.Synthesis.SpeechSynthesizer]: verify that the assembly containing this type is loaded.

I suspect this class is not part of .NET Core.

You're correct @doctordns, but note that PowerShell Core lets you load the assembly from the .NET Framework GAC (you need to load the assembly manually even on Windows PowerShell, with Add-Type -AssemblyName System.Speech).

The larger question is why PowerShell Core loads assemblies from the GAC when using Add-Type -AssemblyName with a simple assembly name such as System.Speech, given that they cannot generally assumed to be compatible, and what controls this behavior.

The following works in Windows PowerShell; in Core, the assembly with its types is _loaded_, but, clearly, _using_ the [System.Speech.Synthesis.SpeechSynthesizer] fails, as reported by @1024-Kibibytes.

Add-Type -AssemblyName System.Speech
# In PS Core, outputting [System.Speech.Synthesis.SpeechSynthesizer] by itself shows 
# that the type was loaded, but instantiating it and calling .Speak() then fails.
[System.Speech.Synthesis.SpeechSynthesizer]::new().Speak('This is Windows talking.')

As for a workaround, @1024-Kibibytes (still Windows-only, but also works from PS Core):

(New-Object -ComObject SAPI.SpVoice).Speak('This is Windows talking.')

System.Speech uses reflection to access the non-public field RegistryKey.hkey which was renamed in core to _hkey. Maybe worth opening an issue on dotnet/runtime to inform them that there's a Microsoft assembly depending on that field name.

Thanks, @SeeminglyScience. Can you shed light on the general questions?

  • Why does PowerShell Core load assemblies from the GAC with Add-Type -AssemblyName (with a simple assembly name such as System.Speech), given that assemblies there cannot generally assumed to be compatible, correct?

    • Or is the loading smart to limit itself to assemblies found to indicate .NET Core compatibility? (With the only problem at hand being the one you've explained.)

    • Or does the loading not have to be smart, and does it instead rely on truly incompatible assemblies _failing_ to load?

  • What, if anything, can a user do to control this behavior? (If the loading is smart or consistently fails in case of incompatibility, there may be no need for such a feature).

  • Why does PowerShell Core load assemblies from the GAC with Add-Type -AssemblyName (with a simple assembly name such as System.Speech), given that assemblies there cannot generally assumed to be compatible, correct?

There's a pretty reasonable assumption that a large chunk of assemblies will be compatible. You can't really guarantee compatibility of anything you don't provide yourself, but that isn't really new with Core. That said, most assemblies will not be relying on private field names, this is an outlier.

  • Or is the loading smart to limit itself to assemblies found to indicate .NET Core compatibility? (With the only problem at hand being the one you've explained.)

Nah there's no way to tell if an assembly is compatible without manual testing. Also not .NET Core specific.

  • What, if anything, can a user do to control this behavior? (If the loading is smart, there may be no need for such a feature).

Some options, all except the first are sorta complicated:

  • Ship your own copy of the assembly with your module/script/etc
  • Custom AssemblyResolve event
  • Custom AssemblyLoadContext

Thanks for opening a .NET Core issue, @lukeb1961.

Thanks for the explanation, @SeeminglyScience; if you'll indulge my curiosity further:

I now see that even .NET Core itself - not just PowerShell - goes looking for assemblies in the GAC on Windows (with the obsoleteSystem.Reflection.Assembly.LoadWithPartialName() or - using an assembly's _full_ name - with System.Reflection.Assembly.Load())

However, it seems that it only looks in the .NET 4+ GAC ($env:WinDir\Microsoft.Net\assembly), not also in the .NET 3.x- one ($env:WinDir\assembly), the way WinPS does.

Is the .NET 3.x- GAC categorically and intentionally excluded? Some assemblies there at least do _load_ in .NET Core if targeted by their full path (their types are listed with Add-Type -PassThru).

What determines which .NET Framework assemblies .NET Core can use?

My previous understanding was that only .NET Standard assemblies were also usable by .NET Core.

Is the .NET 3.x- GAC categorically and intentionally excluded? Some assemblies there at least do _load_ in .NET Core if targeted by their full path (their types are listed with Add-Type -PassThru).

Oh, I didn't expect that PS would have it's own logic to crawl the GAC. The Fusion API should be considered instead. I'm guessing the current implementation was due to COM interop not being supported at the time, but it is with 3.0.

What determines which .NET Framework assemblies .NET Core can use?

Whether they use any API's that were removed or had breaking changes. Again not so much specifically a framework vs core thing, though a lot more removals and breaks did happen.

My previous understanding was that only .NET Standard assemblies were also usable by .NET Core.

Usually when you see someone say they have to "write a version for netstandard" it really means they depended on something that broke. If none of the API's they used happened to change, it'll often work fine.

Just to clarify, it's a whole lot safer to specifically target netstandard. The standard is just a declaration of API surface that the different runtimes commit to supporting (or at the very least not throwing JIT time errors for).

It's sort like how you can technically use the PS 5.1 version of PowerShellStandard.Library to write code for PSv3. As long as you only actually use API's that also exist in PSv3, it'll be fine. It's just sort of dangerous because you don't get compile time errors for API's that don't exist in your target.

I now see that even .NET Core itself - not just PowerShell - goes looking for assemblies in the GAC on Windows (with the obsolete System.Reflection.Assembly.LoadWithPartialName() or - using an assembly's full name - with System.Reflection.Assembly.Load())

The GAC-probing logic in powershell was added in 6.2 I think, when pwsh targets .NET Core 2.1.
~But I wasn't aware that Assembly.LoadWithPartialName() and Assembly.Load() is now looking in GAC in 3.1. If so, we probably want to revisit that logic.
@mklement0 Can you please point me to the code/docs about the GAC assembly loading behavior in 3.1? /cc @adityapatwardhan~

@daxian-dbw Pretty sure those call your event handler on the default ALC no?

@SeeminglyScience I thought @mklement0 meant those 2 APIs are probing assemblies in the GAC in general, not in the PowerShell context. Did I misunderstand this?

I could be wrong (@mklement0 please correct if I am) but I'm assuming he called those APIs from a PowerShell prompt. It would be easy (and reasonable) to assume that those APIs wouldn't hit any PowerShell specific code.

Basically I'm guessing he didn't make a console app on the off chance that it would work that way.

@SeeminglyScience - good point: I had only tried [reflection.assembly]::LoadWithPartialName("System.Speech") from a PowerShell Core session, and I can confirm that it indeed does _not_ work from a console application.

@SeeminglyScience @mklement0 Thank you guys for the clarification! I appreciate it!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manofspirit picture manofspirit  路  3Comments

alx9r picture alx9r  路  3Comments

rkeithhill picture rkeithhill  路  3Comments

concentrateddon picture concentrateddon  路  3Comments

JohnLBevan picture JohnLBevan  路  3Comments