I am currently learning myself a little F# and have written the following code to practice.
The code uses the Mono.Cecil library to inject some IL at the start of every method in each .net assembly found in a given directory. The IL will call a LogMe method in a Loggit.dll assembly.
I want to make this code as functional and idiomatically F# as possible.
My main concern is that the method to inject code into an assembly has some definite side effects. This is unavoidable as thats what the whole point is. I do this in the function InjectMethods. This function is passed to the DoOnModule function.
This DoOnModule function is used twice -
- once when injecting methods - a method with side effects and no real return value,
- and once when retreiving the LogMe method reference from the Loggit.dll assembly - a method with no side effects and a useful return value.
I feel a little uneasy about this double use, which doesnt feel very functional - and yet I find it very useful save code repetition...
How can I make this code more functional?
//
open System.IO
open Mono.Cecil
open Mono.Cecil.Cil
exception LogNotFoundError of string
let IsClass(t:TypeDefinition) =
t.IsAnsiClass && not t.IsInterface
let UsefulMethods(t:TypeDefinition) =
t.Methods |> Seq.filter ( fun m -> m.HasBody )
// Perform the given funtion on the module found at the given filename.
let DoOnModule fn (file:string) =
try
let _module = ModuleDefinition.ReadModule file
Some ( fn ( _module ) )
with
| :? System.BadImageFormatException as ex -> printfn "%A" ex; None
| :? System.Exception as ex -> printfn "%A" ex; None
// Do the given function on the dll filenames found in the given directory
let MapAssemblies fn directory =
System.IO.Directory.GetFiles(directory) |>
Seq.filter(fun file -> file.EndsWith("dll") || file.EndsWith("exe") ) |>
Seq.map( fn )
// Return the methods found in the given module
let GetMethods(_module:ModuleDefinition) =
_module.Types |> Seq.filter ( IsClass ) |> Seq.collect ( UsefulMethods )
// Get the log method found in the Loggit.dll.
// A call to this method will be injected into each method
let LogMethod (_module:ModuleDefinition) =
let GetLogMethod(_logmodule:ModuleDefinition) =
let logClass = _logmodule.Types |> Seq.filter ( fun t -> t.Name.Contains ( "Log" ) ) |> Seq.head
let logMethod = logClass.Methods |> Seq.filter ( fun m -> m.Name.Contains ( "LogMe" ) ) |> Seq.head
_module.Import logMethod
"Loggit.dll" |> DoOnModule GetLogMethod
// Injects IL into the second method to call the first method,
// passing this and the method name as parameters
let InjectCallToMethod(logMethod:MethodReference) (_method:MethodDefinition) =
let processor = _method.Body.GetILProcessor()
let firstInstruction = _method.Body.Instructions.Item(0)
let parameter1 = processor.Create(OpCodes.Ldstr, _method.Name)
let parameter2 = processor.Create(if _method.HasThis then OpCodes.Ldarg_0 else OpCodes.Ldnull)
let call = processor.Create(OpCodes.Call, logMethod)
processor.InsertBefore ( firstInstruction, parameter1 );
processor.InsertBefore ( firstInstruction, parameter2 );
processor.InsertBefore ( firstInstruction, call )
// Inject a call to the Log method at the start of every method found in the given module.
let InjectMethods(_module:ModuleDefinition) =
// Inject the call
let logMethod = LogMethod _module
match logMethod with
| Some(log) ->
let methods = GetMethods _module
for m in methods do
m |> InjectCallToMethod log
| None -> raise(LogNotFoundError("Cant find log method"))
// Save the module
Directory.CreateDirectory ( Path.GetDirectoryName ( _module.FullyQualifiedName ) + @"\jiggled\" ) |> ignore
_module.Write ( Path.Combine ( Path.GetDirectoryName ( _module.FullyQualifiedName ) + @"\jiggled\", Path.GetFileName ( _module.FullyQualifiedName ) ) );
let dir = "D:\\Random\\AssemblyJig\\spog\\bin\\Debug"
// Now inject into the methods
try
dir |>
MapAssemblies ( DoOnModule InjectMethods ) |>
Seq.toList |>
ignore
with
| :? System.Exception as ex -> printfn "%A" ex;
System.Console.ReadLine |> ignore
1 Answer 1
I only have two minor nitpicks with this code:
Seq.filter(fun file -> file.EndsWith("dll") || file.EndsWith("exe") ) |>
Perhaps this should say ".dll"
and ".exe"
. Or you could use Path.GetExtension
.
Seq.filter ( fun t -> t.Name.Contains ( "Log" ) ) |> Seq.head
This will take the first type whose name happens to contain Log
; even if the name is ILoggable
or LogProcessor
or something. Are you sure you know that there will never be any other types with such names than the type you want? The same goes for the method LogMe
.
-
\$\begingroup\$ Also using Path.Combine is better than concatenating "\jiggled\" etc. \$\endgroup\$Francesco De Vittori– Francesco De Vittori2012年05月02日 10:36:04 +00:00Commented May 2, 2012 at 10:36
-
\$\begingroup\$ Wouldn't Seq.find(...) be a shorter and clearer version of Seq.filter(...) |> Seq.head? \$\endgroup\$Mathias– Mathias2012年11月23日 21:44:35 +00:00Commented Nov 23, 2012 at 21:44