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