Search code examples
powershellclasscompilationinterpreterpowershell-module

Understanding 'Powershell Runtime' and 'PowerShell Compile Time', Optimal method for sharing a class across multiple modules and scripts


So I finally finished building a class/type but now I am stumped on how to actually deploy it at the places I need. The class is supposed to provide a general purpose type, called Filmcode. I have a number of modules that I want to utilise this type with, as well as some some stand alone scripts.

I have been following the excellent book, Windows PowerShell in action (its actually PowerShell 6) by Bruce Payette and Richard Siddaway. Chapter 19 focuses on classes, and he talks about this importing subject:

19.4. Classes, modules, using, and namespaces

Now you know a lot about classes, but you still need to see how they’re organized for use and reuse. The fundamental element of reuse is, as always, the PowerShell module. You’ll organize your classes into modules and then use those modules in your scripts. The difference comes in how you use those modules. This is where another significant difference with classes shows up. Whereas most things in PowerShell are resolved at runtime, PowerShell classes are processed at compile time. When you want to get all the type-checking benefits that classes provide, particularly IntelliSense support, it’s necessary for PowerShell to know about classes ahead of runtime.


He talks about "most things in PowerShell are resolved at runtime" and "PowerShell classes are processed at compile time". I am so confused by how these terms are being used here.

But PowerShell is a interpreted language and I am using native Powershell code, so what is being compiled? Even then, if I just wanted to follow his guide, When does this "compile time" step occur? Is it when I launch the pwsh.exe process? or when I import the module, such as using it or manually importing it?

I think if I just understood "runtime" and "compile time", as they are being used in this book, and when they occur. I would know how to go about things.

I of course, can just paste the raw code for the class on the bottom of every ".ps1" or ".psm1" but this is not ideal, I want to learn how to do it the proper way

I searched for these two keywords but turned up with nothing relevant with regards to Powershell. Any insight would be most wellcome.

I am on Powershel 7.4


Solution

    • Fundamentally, compilation in PowerShell is an implementation detail:

      • Behind the scenes, on-demand, in-memory-only compilation occurs.

        • PowerShell v3 introduced compilation for performance optimization, as described in this answer.

        • Custom classes, as well as enums introduced in v5, of necessity must be compiled to an (in-memory) .NET assembly in order to act like regular .NET types.
          Note: For brevity, only classes are referred to in the remainder of this answer, but everything applies analogously to enums too.

      • (The only exception is if user code explicitly creates an on-disk assembly from embedded C# code with Add-Type -OutputAssembly.)

    • To scripters (users who write PowerShell code), the distinction that matters is between parse time and runtime:

      • Parse-time processing refers to the static, up-front analysis of a piece of PowerShell code, notably to ensure its syntactic correctness, to create an intermediate bytecode representation of the source code (as discussed in the linked answer), and to compile PowerShell class definitions.

        • The design rationale for, technical background on, and constraints around needing to compile PowerShell classes at parse-time are discussed in the initial post of GitHub issue #6652, which is a meta issue tracking various unresolved custom class issues.

        • Fundamentally, when .NET types are referenced via type literals (e.g. [Foo]) in the parts of the code that are relevant at parse time, these types must already exist, i.e. must either already have been compiled in memory (in the case of PowerShell classes) or, for .NET types from on-disk .NET assemblies that aren't automatically loaded by PowerShell itself on startup, must already have been loaded.

          • The parse-time-relevant parts all relate to class definitions; specifically:

            • Use as the base class to derive from or interface to implement in a class definition (e.g.
              class Bar : Foo { <# ... #> })

            • Use as a parameter or return type in in a class definition (e.g.
              class Bar { DoSomething([Foo] $foo) { <# ... #> } })

          • Note that while using a type literal to type a parameter in a script file or script block's top-level param(...) block (e.g. param([Foo] $Foo)) is in effect subject to the same must-already-exist requirement, it is technically a runtime error.

      • Runtime processing, i.e. actual execution occurs afterwards, and only if the parse-time stage succeeded.


    Implications of the above, limitations as of PowerShell 7.4:

    • Since Import-Module calls and even #Requires -Modules directives only execute at runtime, a parse-time import statement is required in order to make class definitions from imported modules available to the caller under all circumstances.

      • using module is the parse-time equivalent of an Import-Module call, and it is the only way to make a module's class definitions - invariably all of them - available to the caller, including at parse time.[1]
    • Unfortunately, the seemingly analogous using assembly statement is - as of PowerShell (Core) v7.4 - not processed at parse time. That is, even though it should be, it is currently not the parse-time equivalent of Add-Type calls (with -Path / -LiteralPath or -AssemblyName).

      • Therefore, you can not currently use it to reference .NET types from on-disk assemblies so as to use those types in class definitions.

      • Fixing this problem has been green-lit in GitHub issue #3641 a long time ago, but no one has stepped up to implement it yet (which won't be trivial).

      • There are two additional using assembly problems present as of v7.4.0:

        • Inability to use it to load well-known assemblies, such as System.Windows.Forms - see GitHub issue #11856

        • On Unix-like platforms, the inability to use relative paths - see GitHub issue #20970

    • Similarly, as of v7.4 you also cannot refer to a class definition defined in the same file from class definitions, be it in the context of that same class (notably when declaring interface implementations, e.g.
      class Foo : System.Collections.Generic.IEnumerable[Foo] { <# ... #> }) or in the context of a separate class definition in the same file.


    [1] Import-Module long predates the v5+ class support, and supports exporting only functions, aliases, and variables. using module invariably exports class definitions, and given that PowerShell class definitions lack C#-style access modifiers such as private, invariably exports all of them. However, while public .NET types (which is what PowerShell classes invariable are) are normally seen session-wide, and even though using module imports a module session-globally too, its PowerShell class definitions are visible only to the using module caller (and its descendent scopes).