Scripting for .NET

Alex Angelopoulos (aka at mvps dot org)

There are several ways to exploit the .NET redistributable for scripting use.  The two simplest are writing a console application and creating a COM-callable component using the .NET CLR.

The Issues To Consider

Based on prior experience with a few programming languages and a good background with scripting, I think if you are contemplating using .NET for scripting support, you need to ask yourself a few questions first.

Is it universally usable for you?

One of the values of scripting is the ability to reuse and redeploy solutions.  At this point, every commercially installed Microsoft operating system released since 1995 supports WSH and COM objects compiled in Visual Basic 5/6 and Visual C++ 5/6.  More importantly, the necessary runtime libraries are installed almost everywhere.

Using .NET requires installation of the 20+ MiB redistributable, and it will NOT install on Windows 95 or on the Terminal Services version of NT4.  If those aren't problems for you, then this isn't an issue.  If there are concerns about install capability, I would recommend sticking with slightly older tools.

What programming language do you use and how familiar with it are you?

If .NET is an option, the next thing to consider is what language you intend to use; it does indeed make a difference.

If you are a C/C++ user, many people seem to suggest that the leap to C# is not only fairly easy but fun: the tools are much easier to exploit.  Getting COM interoperability to work is not incredibly straightforward, but it can be done even with just the redistributable.

By the same token, if you use Jscript for scripting, Jscript.NET can be fun and easy to pick up.

VB.NET is a different story for various reasons.  As a heavy user of Visual Basic 5/6 over the last year or so, I have come to appreciate some of the ability to delve into the guts of Windows that .NET can give you.  Unfortunately, VB 5/6 code which performs external manipulation does not port to VB.NET easily; anything that uses API calls, for example, will require significant rewriting.  After that job is done, you will be in a worse position for COM interoperability!

For the near future, I would recommend sticking with VB5/6 if you're a VB aficionado.  In my opinion, it is fun if you're a hard-core programmer and are focused on non-scripting issues, but for scripting support you are talking about learning a less-universal framework, porting working code to it, familiarizing yourself with a more stringent language, and then having less support for scriptability than when you started.

What About C++?

One minor annoyance is that if you are a C++ user, Microsoft does not make a C++ one available for you. (Before .NET we had free no compilers from Microsoft at all, so I'm not going to complain about this...<g>).

It won't give you a free managed C++, but you CAN use Borland's C++ compiler as a free C++ tool.  Also, there is a version of LCC which runs with the .NET environment as a back end.

The .NET Framework Itself: Where It Lives, What It Is

There are quite a few descriptions of the .NET framework available from various resource sites on the Internet, not to mention popular books and articles.

Few approach it from the perspective of a network administrator, who often finds it convenient to begin looking at tools as particular files of particular types installed at a particular location.

The Framework installs itself within the system's Windows directory; the core of it is a set of DLLs along with important associated executables in a version-named subfolder.  In my particular case, this path is C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705.

The exciting thing about .NET is this: ALL of the compilers are installed with it.  When you install the .NET redistributable, you also get (depending on your version) the following executables:

Executable Description
al   Assembly Linker
csc   Visual C# .NET Compiler
cvtres   Windows Resource To Object Converter
ilasm   .NET Framework IL Assembler
jsc   Jscript .NET Compiler
ngen   CLR Native Image Generator
RegAsm   .NET Framework Assembly Registration Utility
vbc   Visual Basic .NET Compiler
vjc   Visual J# .NET Compiler


 

Writing a Console Application

One of the oldest tricks around for high-level applications on Unix/Linux boxes has been to allows users to extend them by creating compiled applications on the fly.

Since the .NET redistributable include compilers for C# and VB.NET, this trick can be exploited by WSH users as well now. All you need is the .NET redistributable (NOT the SDK or development framework). Below is a simple wrapper class that puts code you give it into a VB.NET console application, compiles it, runs it and captures the output, then deletes the source file and exe created.

VB.NET source code - this is already in the script below in escaped form. It just demos calling the GetTickCount API to give an idea of what can be done with this technique. It DOES make API calls pretty easy...

Module Main
Public Declare Function GetTickCount Lib "kernel32" () As Long
Sub Main()
System.Console.WriteLine(GetTickCount)
End Sub
End Module

Below is a VBScript which generates a .NET console application on the fly.

' VBNetWrap.vbs
' you can get the code a variety of ways, including reading a file
' this is just to wrap this all into a package...
code = "Module%20Main%0D%0APublic%20Declare%20Function%20" _
& "GetTickCount%20Lib%20%22kernel32%22%20%28%29%20As%20Long" _
& "%0D%0ASub%20Main%28%29%0D%0ASystem.Console.WriteLine%28" _
& "GetTickCount%29%0D%0AEnd%20Sub%0D%0AEnd%20Module"

' there are only 3 steps to dynamically creating and using a
' .NET console application from WSH with the class below.

' 1 - instantiate the class
set vbn = new VbNetApp

' 2 - add source code for a COMPLETE console application to it
vbn.code = unescape(code)

' 3 - execute!
WScript.Echo vbn.exec


Class VbNetApp

dim sh, fso
dim m_base, m_code, m_cmd

public property let code(sData)
'sets the code to use
m_code = sData
end property

public property get code()
code = m_code
end property

private sub class_initialize
Set sh = createobject("WScript.Shell")
Set fso = createobject("Scripting.FileSystemObject")
m_base = RandomName8
m_cmd = "vbc /nowarn /nologo /quiet /debug- " & m_base _
& ".vb && " & m_base & ".exe"
end sub

private function Cmd(cmdline)
' Wrapper for getting StdOut from a console command
Dim fOut, OutF, sCmd
fOut = fso.GetTempName
sCmd = "%COMSPEC% /c " & cmdline & " >" & fOut
Sh.Run sCmd, 0, True
If fso.FileExists(fOut) Then
If fso.GetFile(fOut).Size>0 Then
Set OutF = fso.OpenTextFile(fOut)
Cmd = OutF.Readall
OutF.Close
End If
fso.DeleteFile(fOut)
End If
End Function

Public Function Exec
' writing, execution, and deletion
' all occur in the same call to ensure
' that directory changes don't occur.
WriteFile m_base & ".vb", m_code
Exec = Cmd(m_cmd)
fso.DeleteFile m_base & ".vb", true
fso.DeleteFile m_base & ".exe", true
End Function

Private Sub WriteFile(FilePath, sData)
'writes sData to FilePath
With fso.OpenTextFile(FilePath, 2, True)
.Write sData
.Close
End With
End Sub

private function RandomName8
' Returns unique 8-character name
' Following sequence allows 5,352,009,260,481 unique items
chrList = _
"abcdefghijklmnopqrstuvwxyz0123456789~-_"
uLimit = Len(chrList)
Randomize
For i = 1 To 8
sTmp = sTmp & Mid(chrList, ((uLimit) * Rnd + 1), 1)
Next
RandomName8 = sTmp
End Function

End Class

Creating COM-Callable .NET Components with the .NET Redistributable

This is one of the cleaner methods for creating COM-accessible components written in VB.NET, C#, or Jscript.NET.

The process involves compilation of a simple class file, then registering its codebase.  This means there is minimal name protection, so be certain to use a long, unique class name! The lack of strong naming means this is best targeted at on-the-fly creation of "anonymous" COM classes which will be removed after use; in fact, as I try to generalize the technique, it will be best to include strong naming as a feature.

In any case, below is a generic batch file for creating the DLLs, followed by brief demos in each of the 3 compilers mentioned.

Step 1: Compile into a COM-Callable Library

There are two simple steps: compile to a library, and then register the
codebase.  The batch file shown below will do this given a source file name as
an argument; it determines which compiler to call based on the source file's
extension.  The compiled DLL will have the same base name as the source file.

@echo off
:: generic .NET library compiler script

:: get the extension into %ext%
set srcfile=%1
set ext=%srcfile:~-2%

:: get the basename into %basename%
CALL SET basename=%srcfile:~0,-3%%
echo basename is %basename%

:: set the correct compiler
set exc=%ext%c

%exc% /nologo /t:library %basename%.%ext%
regasm /nologo /codebase %basename%.dll

Step 2: Run A Script Calling The Library

If the class is named "thisismyclass" and it exposes a public function "testfunction", you can run it from VBScript like this:

set cls = CreateObject("thisismyclass")
rtn = cls.testfunction

Demo classfile sources and VBScript demos are included below for the Jscript.NET, C#, and VB.NET.

2.1 Code: Test Jscript.NET Class File

class testcompiledjscriptnet
 {
     public function testreturn()
        {
            // display the number of commandline items
            return("this is a string returned from a Jscript.Net library.");
         }
}

2.1 Script: VBScript to test Jscript.NET Library

set js = CreateObject("testcompiledjscriptnet")
wscript.echo js.testreturn

2.2 Code: Test C# Class File

public class testcompiledcsharpnet
{
public string testreturn()
 {
 return("this is a string returned from a C# library.");
 }
}

2.2 Script: VBScript to test C# Library

set cs = CreateObject("testcompiledcsharpnet")
wscript.echo cs.testreturn

2.3 Code: Test VB.NET Class File

' Test
public class testcompiledvbnet
 public function testreturn() as string
  return("this is a string returned from a VB.Net library.")
 end function
end class

2.3 Script: VBScript to test VB.NET Library

set vb = CreateObject("testcompiledvbnet")
wscript.echo vb.testreturn