4
\$\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
\$\endgroup\$
3
\$\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.

\$\endgroup\$
  • \$\begingroup\$ Also using Path.Combine is better than concatenating "\jiggled\" etc. \$\endgroup\$ – Francesco De Vittori May 2 '12 at 10:36
  • \$\begingroup\$ Wouldn't Seq.find(...) be a shorter and clearer version of Seq.filter(...) |> Seq.head? \$\endgroup\$ – Mathias Nov 23 '12 at 21:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.