6
\$\begingroup\$

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
asked Feb 25, 2011 at 16:30
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

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.

answered Feb 25, 2011 at 16:51
\$\endgroup\$
2
  • \$\begingroup\$ Also using Path.Combine is better than concatenating "\jiggled\" etc. \$\endgroup\$ Commented May 2, 2012 at 10:36
  • \$\begingroup\$ Wouldn't Seq.find(...) be a shorter and clearer version of Seq.filter(...) |> Seq.head? \$\endgroup\$ Commented Nov 23, 2012 at 21:44

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.