Freigeben über


PINVOKE or accessing WIN32 APIs

In the very early days of Monad, we focused on a world where everyone would publish Cmdlets and providers.  Cmdlets and providers were designed around providing the best user experience.  They provide the right, high-level, task-oriented abstractions that users want while also providing the semantic benefits that the Monad engine provides (-WHATIF, -CONFIRM, -VERBOSE, -ERRORACTION, -ERRORVARIABLE, etc etc).  The trick then is - how do we get full coverage with Cmdlets?

To address this issue - we took an economic approach: require a tiny amount of code, provide a huge amount of function and good things will occur.  Anyone that has written a Cmdlet gets this in focus very quickly.  We provide value-add functions in a variety of ways.  First, Cmdlets are subclassed from our base class and then hosted by the Monad engine.  The base class and the engine provide a ton of functionality for free when you use the right APIs.  For instance, all you have to do is call ShouldProcess() and the engine will implement -WHATIF -CONFIRM and -VERBOSE for you.  Good deal. 

We also provide a ton of functions to manipulate  the objects that your cmdlets emit.  We have object utilities (sort, where, group, select, compare, etc), we have formating and outputing commands (format-table/list/wide/custom, out-host/file/printer/string etc, export/import-clixml/csv), we allow 3rd parties and end-users to extend your objects (*format.mshxml, *types.mshxml), we provide an interactive shell and a programming language with an awesome dynamic range which is usable by beginner scripters to very sophisticated programmers.  All and all, I think you'd be hard-pressed to get more functions per line of code than a Cmdlet provides (providers give even more functionality but they are not as tiny as Cmdlets). 

While we think we've produced a compelling economic,  it didn't take very long for us to do the analysis and realize that would quite a while before customers had full coverage for all the functions that they needed.  It would take a while before vendors had an opportunity to update their products, it would take a while before customers adopted the new versions of those products, and in the end - there might be some vendors that won't write Cmdlets no matter what (for technical, strategic, geo-political, or religious reasons or maybe because the vendor went out of business).  At this point, we expanded the scope of the project to embrace existing forms of instrumentation. 

Today we support Text Processing, COM automation, access to .NET classes but currently there is no native support for Win32 APIs.   That said, .NET provides some great support for calling Win32 APIs.  Here is a pointer to a good article on the details of .NET's support for PINVOKE (Platform Invoke) :

https://msdn.microsoft.com/library/default.asp?url=/library/en-us/csref/html/vcwlkplatforminvoketutorial.asp

Below is a code sample for how you can write an MSH Script with embedded C# code to invoke a Win32API.

#######################################################################
# This is a general purpose routine that I put into a file called
# LibraryCodeGen.msh and then dot-source when I need it.
#######################################################################
function Compile-Csharp ([string] $code, $FrameworkVersion="v2.0.50727",
[Array]$References)
{
#
# Get an instance of the CSharp code provider
#
$cp = new-object Microsoft.CSharp.CSharpCodeProvider

#
# Build up a compiler params object...
$framework = Combine-Path $env:windir "Microsoft.NET\Framework\$FrameWorkVersion"
$refs = new Collections.ArrayList
$refs.AddRange( @("${framework}\System.dll",
"${mshhome}\System.Management.Automation.dll",
"${mshhome}\System.Management.Automation.ConsoleHost.dll",
"${framework}\system.windows.forms.dll",
"${framework}\System.data.dll",
"${framework}\System.Drawing.dll",
"${framework}\System.Xml.dll"))
if ($references.Count -ge 1)
{
$refs.AddRange($References)
}

$cpar = New-Object System.CodeDom.Compiler.CompilerParameters
$cpar.GenerateInMemory = $true
$cpar.GenerateExecutable = $false
$cpar.OutputAssembly = "custom"
$cpar.ReferencedAssemblies.AddRange($refs)
$cr = $cp.CompileAssemblyFromSource($cpar, $code)

if ( $cr.Errors.Count)
{
$codeLines = $code.Split("`n");
foreach ($ce in $cr.Errors)
{
write-host "Error: $($codeLines[$($ce.Line - 1)])"
$ce |out-default
}
Throw "INVALID DATA: Errors encountered while compiling code"
}
}

###########################################################################
# Here I leverage one of my favorite features (here-strings) to define
# the C# code I want to run. Remember - if you use single quotes - the
# string is taken literally but if you use double-quotes, we'll do variable
# expansion. This can be VERY useful.
###########################################################################
$code = @'
using System;
using System.Runtime.InteropServices;

namespace test
{
public class Testclass
{
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();

public static void Run(string message)
{
puts(message);
_flushall();
}
}
}
'@

##################################################################
# So now we compile the code and use .NET object access to run it.
##################################################################
compile-CSharp $code
[Test.TestClass]::Run("Monad ROCKS!")

Enjoy

Jeffrey P. Snover [MSFT]

Comments