Какие бы хвалебные оды не пелись в адрес PowerShell, всегда найдется тот, кто подбросит дегтя в боченок с медом. Нет, я не имею в виду себя, так как в виду своей природы мне непонятны все эти словесные перепалки между людьми, культивирующими те или иные операционные системы, командные шеллы и прочее, — еще ничто из того, что было бы создано человеком, не было совершенным, да и вряд ли таковое когда-либо случится, так как нельзя удержать в поле зрения абсолютно все аспекты предмета, не говоря уже о том, что в некоторых из них человек может оказаться не сведущ вовсе. Предмет — всего лишь средство в достижении поставленой цели, насколько эффективно он используется — это уже вопрос рационального подхода к его характеристикам.
Предел — это более психологический барьер, нежели факт. Когда кто-то говорит, что достиг предела в некотором из своих начинаний, можно с уверенностью утверждать, что человек не добился ровным счетом ничего, а некоторый результат — всего лишь промежуточное состояние предмета. Возможно, кто-то припомнит избитую поговорку «нет предела совершенству», на что можно парировать «ничто не совершенно»; совершенство по сути — недостижимая цель, которую человек себе ставит очевидно лишь для того, чтобы наполнить жизнь смыслом. Впрочем, это все риторика, имеющая к делу лишь посредственное отношение.
Развитие PowerShell планомерно, в смысле разработчики изначально заложили в него прочный фундамент, возводя с каждой последующей версией не менее твердую конструкцию. И все же лично мне кажется, что некоторые вещи в PowerShell развиваются несколько не в том направлении. Например, если с отсутствием возможности создавать перечисления мириться можно (в виду наличия хэштаблиц), то как быть со структурами? Разумеется и перечисления и структуры могут быть созданы посредством командлета Add-Type, но лично мне этот способ кажется топорным из-за его расхода времени на компиляцию кода. Найдется еще с десяток прочих аргументов не в пользу использования данного командлета, но соль не в этом. Создатели PowerShell весьма дальновидно предусмотрели расширяемость последнего за счет модулей (если мне не изменяет память, эта возможность появилась во второй версии), тем самым стимулируя разработчиков на различного рода эксперименты как с функциональностью, так и синтаксисом.
Мои эксперименты с PowerShell начинались в пору первой версии последнего. Тогда мной было предпринято несколько попыток расширения возможностей PowerShell за счет компиляции C#-кода, так как командлета Add-Type еще не было; чуть позже возникло желание расширить синтаксис самого PowerShell, но эта затея стала принимать вполне осязаемые черты лишь с переходом на вторую версию, — ключевую роль здесь сыграли именно модули. Впрочем, на все имеющиеся на данный момент наработки повляла одна специфическая черта PowerShell — диски.
Согласно официальной документации диск в PowerShell представляет собой хранилище данных, доступ к которому аналогичен тому, как если бы мы обращались к объекту файловой системы. Я не стану подробно останавливаться на описании каждого диска в отдельности — детали в документации, поясню концепцию легшей в основу идеи расширения синтаксических возможностей PowerShell.
В языках программирования под словом функция разумеется блок инструкций, в то время как в PowerShell функция — это диск, хранящий определение функции в виде пары имя-скрипт-блок. Это проще продемонстрировать на примере.
PS C:> function add($a, $b) { $a + $b }
PS C:> dir function:
PS C:> #или чтобы отсеять ненужное
PS C:> dir function:add
Чтобы посмотреть содержимое функции, используем командлет Get-Content.
PS C:> gc function:add
param($a, $b)
$a + $b
Убеждаемся, что содержимое является скрипт-блоком.
PS C:> (gc function:add).GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ScriptBlock System.Object
Иными словами объявление функции в PowerShell является своего рода иллюзией функции в привычной для последней трактовке, а смысловую нагрузку на себя принимает именно скрипт-блок.
PS C:> add 10 20
30
PS C:> (gc function:add).Invoke(10, 20)
30
Так как функция — это диск, следовательно объявление функции в PowerShell в сущности является записью данных на этот диск.
PS C:> sc function:add { param($a, $b) $a + $b }
Такая запись избыточна, так как все накладные расходы при традиционном объявлении функции хост берет на себя, здесь эта запись приводится для понимания сути (лично я использую подобную запись, чтобы отделить функции с составным именем от простых).
PS C:> function Add-Something { ... } #составное имя
PS C:> sc function:done { ... } #простое имя
Все это занимательно, но какое отношение это имеет к расширению синтаксиса PowerShell? Как я уже говорил, за основу была взята концепция, о которой только что было рассказано, реализация же строится относительно понятия динамической сборки в текущем домене приложений. Давайте посмотрим на следующий код.
Set-Content function:dynmod {
$name = -join (0..7 | % {$rnd = New-Object Random}{
[Char]$rnd.Next(97, 122)
})
if (!($asm = ($cd = [AppDomain]::CurrentDomain).GetAssemblies() | ? {
$_.ManifestModule.ScopeName.Equals(($mem = 'RefEmit_InMemoryManifestModule'))
})) {
($cd.DefineDynamicAssembly(
(New-Object Reflection.AssemblyName($name)), 'Run'
)).DefineDynamicModule($name, $false)
}
else { $asm.GetModules() | ? {$_.FullyQualifiedName -ne $mem} }
}
Функция (мы то знаем, что на самом деле скрипт-блок) создает или обращается к уже созданному модулю в динамической сборке в текщем домене приложений. Сама по себе она мало что значит и в сущности является связующим звеном между хостом и прочими функциями, которые мы в дальнейшем определим. Например, давайте упростим себе вызов API'шных функций за счет подобия C#-делегатов.
#обертка над инкапсулированными функциями GetModuleHandle и GetProcAddress
function Get-ProcAddress {
[OutputType([IntPtr])]
param(
[Parameter(Mandatory=$true, Position=0)]
[String]$Dll,
[Parameter(Mandatory=$true, Position=1)]
[String]$Function
)
$href = New-Object Runtime.InteropServices.HandleRef(
(New-Object IntPtr),
[IntPtr]($$ = [Regex].Assembly.GetType(
'Microsoft.Win32.UnsafeNativeMethods'
).GetMethods() | ? {
$_.Name -match 'AGet(ModuleH|ProcA).*Z'
})[0].Invoke(
$null, @($Dll)
))
if (($ptr = [IntPtr]$$[1].Invoke($null,
@([Runtime.InteropServices.HandleRef]$href, $Function)
)) -eq [IntPtr]::Zero) {
throw (New-Object Exception("Could not find $Function entry point in $Dll library."))
}
return $ptr
}
#какбы новое ключевое слово - delegate
Set-Content function:delegate {
[OutputType([Type])]
param(
[Parameter(Mandatory=$true, Position=0)]
[String]$Dll,
[Parameter(Mandatory=$true, Position=1)]
[String]$Function,
[Parameter(Mandatory=$true, Position=2)]
[Type]$ReturnType,
[Parameter(Mandatory=$true, Position=3)]
[Type[]]$Parameters
)
$ptr = Get-ProcAddress $Dll $Function
$Delegate = $Function + 'Delegate'
if (!(($mb = dynmod).GetTypes() | ? {$_.Name -eq $Delegate})) {
$type = $mb.DefineType(
$Delegate, 'AnsiClass, Class, Public, Sealed', [MulticastDelegate]
)
$ctor = $type.DefineConstructor(
'HideBySig, Public, RTSpecialName', 'Standard', $Parameters
)
$ctor.SetImplementationFlags('Managed, Runtime')
$meth = $type.DefineMethod(
'Invoke', 'HideBySig, NewSlot, Public, Virtual', $ReturnType, $Parameters
)
$Parameters | % {$i = 1}{
if ($_.IsByRef) { [void]$meth.DefineParameter($i, 'Out', $null) }
$i++
}
$meth.SetImplementationFlags('Managed, Runtime')
[Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
$ptr, ($type.CreateType())
)
}
else {
[Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
$ptr, $mb.GetType($Delegate)
)
}
}
Теперь вызвать некоторую API-функцию стало проще (эдакий clockres).
[Int32]$max = $min = $cur = 0
if ((delegate ntdll NtQueryTimerResolution Int32 @(
[Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType()
)).Invoke([ref]$max, [ref]$min. [ref]$cur) -eq 0) {
'Maximum timer resolution: {0:3f}' -f ($max / 10000)
'Minimum timer resolution: {0:3f}' -f {$min / 10000)
'Current timer resolution: {0:3f}' -f ($cur / 10000)
}
Понятно, что есть свои ограничения и подводные камни, но повторюсь, что это лишь идея.
Как я уже говорил, мне хотелось иметь возможноть создавать структуры прямо в PowerShell, без кода на C#.
#какбы новое ключевое слово - struct
Set-Content function:struct {
[OutputType([Type])]
param(
[Parameter(Mandatory=$true, Position=0)]
[String]$StructName,
[Parameter(Mandatory=$true, Position=1)]
[ScriptBlock]$Definition,
[Parameter(Position=2)]
[Reflection.Emit.PackingSize]$PackingSize = 'Unspecified',
[Parameter(Position=3)]
[Switch]$Explicit
)
if (!(($mb = dynmod).GetTypes() | ? {$_.Name -eq $StructName})) {
[Reflection.TypeAttributes]$attr = 'AnsiClass, BeforeFieldInit, Class, Public, Sealed'
$attr = switch ($Explicit) {
$true { $attr -bor [Reflection.TypeAttributes]::ExplicitLayout }
$false { $attr -bor [Reflection.TypeAttributes]::SequentialLayout }
}
$type = $mb.DefineType($StructName, $attr, [ValueType], $PackingSize)
$ctor = [Runtime.InteropServices.MarshalAsAttribute].GetConstructor(
[Reflection.BindingFlags]20, $null, [Type[]]@([Runtime.InteropServices.UnmanagedType]), $null
)
$cnst = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst'))
$ret = $null
[Management.Automation.PSParser]::Tokenize($Definition, [ref]$ret) | ? {
$_.Type -match 'A(Command|String)Z'
} | % {
if ($_.Type -eq 'Command') {
$token = $_.Content #тип поля
$ft = switch (($def = $mb.GetType($token)) -eq $null) {
$true { [Type]$token }
$false { $def } #поиск типа в динамической сборке
} #switch
}
else {
$token = @($_.Content -split 's') #имя поля, смещение, атрибуты и размер
switch ($token.Length) {
1 { [void]$type.DefineField($token[0], $ft, 'Public') } #пример: UInt32 'e_lfanew';
2 { #структура помечена как Explicit: Int64 'QuadPart 0'; иначе String 'Buffer LPWStr';
switch ($Explicit) {
$true { [void]$type.DefineField($token[0], $ft, 'Public').SetOffset([Int32]($token[1])) }
$false {
$unm = [Runtime.InteropServices.UnmanagedType]($token[1])
[void]$type.DefineField($token[0], $ft, 'Public, HasFieldMarshal').SetCustomAttribute(
(New-Object Reflection.Emit.CustomAttributeBuilder($ctor, [Object[]]@($unm)))
)
}
} #switch
}
3 { #пример: UInt16[] 'e_res ByValArray 10';
$unm = [Runtime.InteropServices.UnmanagedType]$token[1]
[void]$type.DefineField($token[0], $ft, 'Public, HasFieldMarshal').SetCustomAttribute(
(New-Object Reflection.Emit.CustomAttributeBuilder($ctor, $unm, $cnst, @([Int32]$token[2])))
)
}
} #switch
}
} #foreach
#пара полезных методов для создаваемой структуры
$OpCodes = [Reflection.Emit.OpCodes]
$Marshal = [Runtime.InteropServices.Marshal]
$GetSize = $type.DefineMethod('GetSize', 'Public, Static', [Int32], [Type[]]@())
$IL = $GetSize.GetILGenerator()
$IL.Emit($OpCodes::Ldtoken, $type)
$IL.Emit($OpCodes::Call, [Type].GetMethod('GetTypeFromHandle'))
$IL.Emit($OpCodes::Call, $Marshal.GetMethod('SizeOf', [Type[]]@([Type])))
$IL.Emit($OpCodes::Ret)
$Implicit = $type.DefineMethod(
'op_Implicit', 'PrivateScope, Public, Static, HideBySig, SpecialName', $type, [Type[]]@([IntPtr])
)
$IL = $Implicit.GetILGenerator()
$IL.Emit($OpCodes::Ldarg_0)
$IL.Emit($OpCodes::Ldtoken, $type)
$IL.Emit($OpCodes::Call, [Type].GetMethod('GetTypeFromHandle'))
$IL.Emit($OpCodes::Call, $Marshal.GetMethod('PtrToStructure', [Type[]]@([IntPtr], [Type])))
$IL.Emit($OpCodes::Unbox_Any, $type)
$IL.Emit($OpCodes::Ret)
$type.CreateType()
}
else { $mb.GetType($StructName) }
}
Пример (uptime).
$sti = struct SYSTEM_TIMEOFDAY_INFORMATION {
Int64 'BootTime';
Int64 'CurrentTime';
Int64 'TimeZoneBias';
UInt32 'TimeZoneId';
UInt32 'Reserved';
UInt64 'BootTimeBias';
UInt64 'SleepTimeBias';
}
$sti = NtQuerySystemInformation $sti SystemTimeOfDayInformation
'{0:D2}:{1:D2}:{2:D2} up {3} day{4}' -f (
$u = (Get-Date) - [DateTime]::FromFileTime($sti.BootTime)
).Hours, $u.Minutes, $u.Seconds, $u.Days, $(if($u.Days -gt 1){'s'}else{''})
Где NtQuerySystemInformation:
$SYSTEM_INFORMATION_CLASS = @{
...
SystemTimeOfDayInformation = 3
...
}
Set-Content function:NtQuerySystemInformation {
param(
[Parameter(Mandatory=$true, Position=0)]
[Type]$Struct,
[Parameter(Mandatory=$true, Position=1)]
[String]$Class
)
$len = $Struct::GetSize()
$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal($len)
$cls = $SYSTEM_INFORMATION_CLASS[$Class]
if ([Regex].Assembly.GetType('Microsoft.Win32.NativeMethods').GetMethod(
'NtQuerySystemInformation'
).Invoke($null, @($cls, $ptr, $len, $ref)) -eq 0) {
$str = $ptr -as $Struct
}
[Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
return $str
}
Вроде бы начиналось что-то о функциях, а заканчивается кучей кода, — позвольте кое-что прояснить. Будь то делегат или структура, которую мы объявляем, все это заносится в одну единственную сборку (принцип диска); прочий код — попытка автоматизироватьупростить созданиевызов структурAPI-функций. При этом структуры дополняются парой полезных методов (получения размера структуры и конвертации указателя в структуру с помощью оператора as, например, $ptr -as $struc).
Все эти изыскания не появились в одночасье, а являются результатом многих экспериментов и простым желанием упростить написание модулей. Возможно кто-то найдет для себя все это полезным и интересным, а этот пост станет своего рода отправной точкой в дальнейших исследованиях или даже поможет сократить количество набираемого кода.
- О дисках PowerShell
- Встроенная справка — man Get-PSDrive
- О домене приложений
Автор: gregzakharov