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
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 class
es, as well as enum
s 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 class
es are referred to in the remainder of this answer, but everything applies analogously to enum
s 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 class
es 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 class
es) 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).