JoinPoint Language Definition

Luego de unos 15 días intensos sobre mi tesis ya tengo andando una pequeña parte andando de lo que será la nueva API, funcionando e integrada con el JIT.

Antes de comentar como funciona vamos por el primer ejemplo que consta de dos partes. Por un lado una clase común y corriente que simularía ser la aplicación :

using System;
public class Normal
{
    public Normal()
    {
        Metodo1(26);
    }

    public void Metodo1(int i)
    {
        Console.WriteLine("Aca");
    }

    static public void Main(string [] args)
    {
        new Normal();
    }
}

Y por otro una clase que solo tiene definiciones de JoinPoints (pueden estar mezcladas, no hace falta separarlo)

using System;
using Weaving;
public class Unused
{
     [RunBefore, JoinPoint("void", "Normal", "*", ParameterFlag.MATCH_ANY)]
     public void Logger(int i, Context c)
    {
        Console.WriteLine ("Called Before with param "+i.ToString());
    }
}

Lo importante de esta segunda clase son los Attributes que se le agregan a los métodos (en adelante los llamaremos callbacks). RunBefore lo que le indica es cuándo se debe correr el callback : en el ejemplo significa que debe ser antes de la ejecución del JoinPoint (sería el equivalente del BeforeAdvice de AspectJ).

El Attribute JoinPoint lo que hace es materializar el JoinPoint en el JIT, para que el RunBefore lo pueda encontrar. Los parámetros que recibe son (cualquiera de las regex soporta los wildcards ‘*’ y ‘?’) :

  • Return Type regex
  • Class Name regex
  • Method Name regex
  • Flags

Para completar el regex contra el que se matchea, el Weaver toma los argumentos del Callback, en este caso lo que se intenta es anticipar la llamada de cualquier método de la clase Normal que reciba un int como primer parámetro y cuyo return type sea void. El Flag MATCH_ANY indica que pueden haber otros parámetros, pero son ignorados por el callback. Si especificáramos MATCH_EXACT estaríamos buscando un método que tenga exactamente un argumento de tipo Integer.

El útimo parámetro (Context) no se usa para el matching y es donde el callback recibe información del contexto sobre donde se está ejecutando (instancia interceptada, método interceptado, etc).

Si compilamos y ejecutamos el ejemplo veríamos algo como lo que sigue :

[mono] ~/src/ejemplos @ gmcs -out:Normal.exe /r:Mono.Weaving.dll Normal.cs Unused.cs 
[mono] ~/src/ejemplos @ mono Normal.exe
(1) [JIT] class Weaver loaded
(2) [CS] ASSEMBLY LOADED: Normal, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
(3) [JIT] New pattern Defined : void Normal:* (int)
(4) [JIT] Matched void Normal:Metodo1 (int) with void Normal:* (int)
(5) [JIT] No instance object found. We need to create one
(6) [JIT] I'll run callback Unused:Logger (int,Weaving.Context) over instance pointed at 0x62f90
Called After with param 26
Aca

Como se puede apreciar, antes de ejecutar el método Metodo1 se ejecutó el callback y recibió correctamente el parámetro original. Cabe aclarar que el weaving se está haciendo a nivel del JIT, intercalando el assembler generado para la plataforma donde se ejecuta, como expliqué en el pasado.

Paso entonces a describir un poco como funciona. Para empezar hay 2 partes, una en código manejado y otra en el JIT (CS y JIT en el output). La parte manejada se usa solamente para detectar cuando un nuevo Assembly es cargado, via AppDomain.CurrentDomain.AssemblyLoad, y ahí por Reflection se leen todas las clases que hay en el Assembly, se escanean los metodos buscando los joinpoints y se llama un método interno del JIT que es quién finalmente define el nuevo joinpoint en la VM.

Esto esta así porque averiguando como hacerlo directo en C encontré varias referencias en la lista de Mono de que el loader en C es muy complejo (haciendo un trace pasé como por 20 funciones).

Como el weaver debe existir antes de ejecutar la aplicacion, el JIT carga el Assembly Weaving.dll desde el GAC antes de cargar el .exe que se va a ejecutar y justo después de cargar el corlib (que debe ser cargada primero que nadie). Luego una instancia de Weaving.Weaver y la mantiene viva por el resto del ciclo de vida de la aplicación.

Cuando el Loader detecta el Attribute de JoinPoint se llama a Weaver.DefineMethod que se mapea a una
función en C. Ésta es la que emite el mensaje (2) creando la regex y guardando toda la metadata necesaria para ejecutar el matcher después.

Otra tarea del loader es detectar si la sintaxis del callback es correcta, por ahora se valida lo siguiente :

  • Context no definido : Advice need at least one parameter of type Weaving.Context
  • Context de tipo inválido : Advice last parameter type XXXXX. Expected Weaving.Context
  • Callback no puede ser abstracto : Abstract Method can’t be advices
  • Callback no puede ser un constructor : Constructors can’t be advices

Cabe destacar que estas exceptions no pueden ser capturadas en un try/catch y hacen que el programa aborte con un backtrace en la consola.

Una vez inicializado y detectados los JoinPoint, a medida que la aplicación es ejecutada, para cada método ejecutado se hace un scan de los patterns definidos y se testea contra la regex de cada uno, si alguna matchea arma la lista de parametros para el callback (tomando los valores originales con los que fue llamada la función interceptada).

El callback tiene 2 comportamientos, dependiendo si el metodo es static o no. Si es STATIC, se ejecuta como STATIC de manera que es el mismo callback para cualquier match. Esto da un comportamiento similar al Singleton de AspectJ (ideal para un logger por ejemplo).

Ahora, si el callback no es STATIC (como en el ejempo) por cada Signature que matchea se crea una instancia diferentes sobre la cual llamar el callback (ideal para un profiler, para saber cuantas llamadas tiene cada método, cada uno con su counter propio).

Obviamente en los callbacks aplican las mismas restricciones que lo métodos comunes : los statics solo pueden acceder a miembros statics de la clase y los no statics los miembros de esa instancia.

Y más o menos de eso se trata mi tesis :). Aún quedan muchas cosas por pulir, mejorar y documentar. La extracción de parámetros desde el stack no es trivial aunque por suerte el profiler de Mono hace algo parecido y calculo que basándome en ese código pueda sacarlo fácil.

Cómo perder un bit

Un proyecto que hicimos a principio de año es un soft que maneja una PC para una rockola con su respectiva interfaz de hardware (“la caja” de ahora en más) para interactuar con la botonera. En estos días el cliente nos pidió que agregaramos alguna validación para que la caja y el programa se aseguren que son “originales” (no es algo que me agrade agregar, pero el software es del cliente).

En fin, la cosa es que estuve todo el viernes y todo el lunes sin poderlo hacer andar. La caja me respondía perfecto salvo en un caso: cuando me llegaba 0x3F pinchaba. Lo peor es que era un valor que me llegaba sospechosamente seguido para ser aleatorio.

Le dimos mil vueltas con Tulku hasta que finalmente encontramos el problema, que obviamente era yo :), o más bien la implementación de SerialPort de .NET (de Microsoft al menos, el programa no corre en Mono todavía) que tiene defaults ridículos :).

El problema era que yo estaba usando un método que lee todo lo que está pendiente, pero justo ese método pasa los bytes por un Encoder, ¿y a que no adivinan? … Si, el Encoder por default es ASCII7BIT, por lo que me tiraba a la basura el bit más significativo del byte leído, haciendo que no hubiera chance de responder correctamente para terminar el handshaking.

Reemplazando el método por un while y leyendo byte a byte de manera RAW, el problema se solucionó.

Pattern matching made easy

Siguiendo en carrera para ir cerrando problemas para mi tesis en esta oportunidad necesité buscar una manera simple de hacer pattern matching. El uso que le iba a dar era muy simple y no necesitaba mucho poder a la hora de las expresiones que se iban a definir.

La idea es poder definir qué métodos quiere uno interceptar usando algún patrón. Por ejemplo, si queremos ejecutar cierto código antes de cualquier método de la clase Test, simplemente debería bastar con poder especificar “Test:* (*)”. En este caso acepto cualquier método con cualquier cantidad de parámetros (puede ser vacío). O si queremos anticipar las llamadas a ToString podríamos definir “*:ToString ()”.

Haciendo una rápida búsqueda en Mr. G. caí primero en en la documentación de la GNU C Library. Tiene más o menos lo que necesitaba, específicamente la sección de “wildcard matching”, pero no encontré como precompilar el patrón. Es algo requerido ya que se evalúa para cáda método que el JIT está por ejecutar por lo que si quiero que sea más o menos “performante” no puedo darme el lujo.

Estaba a punto de ir por una solución usando expresiones regulares cuando me encontré con que la biblioteca glib soporta “glob-style pattern matching“. Tiene menos operadores que la otra (solo * y ?) pero es más que suficiente para empezar. Como glib es usada por Mono internamente por todos lados no agrega una dependencia extra así que por ahora estoy usándola.

La API es bastante cómoda (aunque siguiendo el fiel estilo de glib los nombres son largos), siendo un simple ejemplo de uso :

GPatternSpec *pattern = g_pattern_spec_new ("Test:* (*)");
g_pattern_match_string (pattern, "Test:Bar ()"); // true
g_pattern_match_string (pattern, "Test:Foo (int)"); // true
g_pattern_match_string (pattern, "System.Console:WriteLine (string)"); // false

En mi código no uso g_pattern_match_string ya que recomiendan que si uno tiene varios wildcards se obtienen mejores resultados pasando también el string invertido, por lo que uso directamente g_pattern_match que acepta más parámetros, incluido el string invertido. Lo bueno es que no tengo que preocuparme por strings multibytes porque hasta ahora no conozco ningún lenguaje que corra sobre .NET que permita usar caracteres extendidos para nombrar clases y métodos :).

Me queda como tarea analizar ambas bibliotecas para ver si efectivamente la implementación de glib es más óptima a la hora de hacer repetidas consultas sobre un patrón.

Generando assembler

En esta oportunidad para mi Tesis necesité ver cómo modificar el assembler generado por el JIT para agregar instrucciones antes y después de la llamada a un método. El código assembler generado originalmente es el siguiente :

00000000 (Test_Bar):
(bb):3
   0:   55                      push   %ebp
   1:   8b ec                   mov    %esp,%ebp
   3:   83 ec 08                sub    $0x8,%esp
   6:   83 ec 0c                sub    $0xc,%esp
   9:   68 c0 8f 05 00          push   $0x58fc0
   e:   e8 e5 fd ff ff          call   fffffdf8 (Test_Bar +0xfffffdf8)
  13:   83 c4 10                add    $0x10,%esp
(bb):1
  16:   c9                      leave  
  17:   c3                      ret    

Mi primer intento fue fácil y consistió en ejecutar una función desde assembler antes de cada método generado por el JIT usando los macros x86_push_reg y x86_call_reg. El primero permite poner un valor en un registro y el segundo hacer un call usando la dirección en el registro como punto de entrada. El paso siguiente fue pasarle un parámetro a la función, cosa que no fue tan fácil :).

Encontré que durante la compilación (en un método llamado mono_codegen) se llama a mono_arch_emit_prolog, que dependiendo de la arquitectura se linkea un método diferente. Como yo estoy haciendo todo para x86, me fui a buscar la función para esta arquitectura.

El método en cuestión se encarga de generar un bloque básico de instrucciones para llamar una función, resolviendo ya el problema del pasaje de argumentos, salvar los registros, asegurar alineaciones de memoria y otro tanto código que no se que hace :). Intenté un buen rato meter assembler a mano ahí sin mucho éxito, generando uno loops y segfaults muy bonitos.

Mi problema de insertar assembler antes del código generado por emit_prolog venía a la deriva hasta que caí en la cuenta que Mono tiene una opción de profiling y me puse a investigarla.

Encontré que si el profiler está activo en un momento se llama a mono_arch_instrument_prolog que permite pasarle una función y un MonoMethod (estructura interna que tiene toda la información de un método en código intermedio) y este genera el assembler para ejecutar esa llamada, que en este caso el profiler usa para contar cuantas veces se llama a cada método, entre otras cosas. Esta función me permitió cumplir mi cometido.

Simplemente agregué un método propio tomando como ejemplo cómo se llama al profiler, y el resultado fue que ahora el assembler generado queda :

00000000 (Test_Bar):
(BB):3
   0:   55                      push   %ebp
   1:   8b ec                   mov    %esp,%ebp
   3:   83 ec 08                sub    $0x8,%esp
   /* Acá empieza lo insertado */
   6:   83 ec 08                sub    $0x8,%esp
   9:   55                      push   %ebp
   a:   68 34 58 2f 08          push   $0x82f5834
   f:   e8 64 6e 87 50          call   50876e78 (Test_Bar+0x50876e78)
  14:   83 c4 10                add    $0x10,%esp
  /* Acá termina */
  17:   83 ec 0c                sub    $0xc,%esp
  1a:   68 c0 9f 05 00          push   $0x59fc0
  1f:   e8 64 fd ff ff          call   fffffd88 (Test_Bar+0xfffffd88)
  24:   83 c4 10                add    $0x10,%esp
(BB):1
  27:   c9                      leave  
  28:   c3                      ret    

De a poco las cosas van andando y se va cerrando el ciclo. Estoy más cerca que hace 2 semanas de poder ejecutar algo útil :P.

Mono Internal Calls

Para mi tesis una de las cosas que tenía que lograr era pasar datos desde una aplicación escrita en C# (en realidad en cualquier lenguaje soportado) y el JIT. No quiero entrar en mucho detalle del por qué tengo que hacerlo ni cómo (porque la verdad esto último todavía no lo tengo resuelto :P).

Empezando a investigar caí en Embedding Mono, un articulo donde explican cómo embeber Mono en tu aplicación, por ejemplo para permitir plugins en .NET. El artículo tiene poco que ver con lo que yo estoy haciendo, pero me orientó en mi tarea.

Con Mono tenemos dos formas de ejecutar código nativo : P/Invoke e Internall Calls. El primer método es bastante usado para hacer wrappers de bibliotecas por lo simple que resulta usarlo, además de agregar algunos chequeos en la conversión de datos entre ambos mundos. El segundo es más bien el que me interesa a mi, ya que da un control más de bajo nivel y tiene menos overhead.

La forma de declarar en una clase de C# que un método está implementado en C es declarar un método externo y aplicarle un Attribute para marcarlo como Internal Call. No encontré que otro tipo de métodos externos existe, por lo que no entiendo bien por qué hay que poner ambas cosas :

using System;
using System.Runtime.CompilerServices;
	
class Hello {
  [MethodImplAttribute(MethodImplOptions.InternalCall)]
  extern static string Sample ();
}

Y por otro lado debemos decirle a la VM qué método nativo es el que se debe ejecutar :

/* Somewhere in a C program */
mono_add_internal_call ("Hello::Sample", sample);

Con eso en general basta y funciona. Pero … siempre hay un pero :). Preguntando por IRC me comentaron que esto está pensando, justamente, para los que quieren embeber el runtime de Mono en su aplicación, y no para extender la VM. En mi caso que necesito extender la Common Object Runtime Library (corlib.dll) y la forma “correcta” sería entonces como sigue a continuación.

Declarar una Internal Call (ICALL) que exista desde el inicio no fue tan tan trivial como parecía. Para empezar tuve que navegar entre muchos macros y un código en C poco documentado :).

Finalmente entendí lo que debía hacer y el resultado fue agregar el siguiente código en el archivo icall-def.h :

/* mono/metadata/icall-def.h */
/* Debe estar ordenado alfabeticamente */
ICALL_TYPE(MESSAGE, "System.Message.Message", MESSAGE_1)
ICALL(MESSAGE_1, "DefinePattern", icall_System_Message_DefinePattern)

Al principio lo definí al final de todo, para mantener separado mi código del de Mono, pero cuando ejecuté por primera vez mi ejemplo se quejaba que “System.Message.Message” debía estar después de “System.Buffer”. Investigando un poco encontré que hacen una búsqueda binaria para los métodos internos que son parte de la VM y por eso el requerimiento de estar ordenado.

Al parecer la diferencia con el primer método es que usa un vector estático (que se crea con los macros) y cuando llamamos a mono_add_internal_call usa una GHashTable. La ventaja del array estático es que todo se resuelve en tiempo de compilación, cuando con el hash es todo en runtime.

El macro ICALL_TYPE nos permite empezar a definir un tipo que va a tener una ICALL, teniendo que especificar en el segundo parámetro el nombre completo (es decir, con el namespace) de la clase. El tercer parámetro es el nombre de la primer definición de los métodos que tiene esta clase.

Con ICALL luego definimos para cada método, que función debe ejecutarse en C, siendo en este caso :

void icall_System_Message_DefinePattern(MonoAppDomain *ad, MonoString *pattern)
{
  char *str = mono_string_to_utf8 (pattern);
  g_print ("New pattern Defined : %sn", str);
}

Para poder usar esto desde C# agregué un namespace a System, algo muy tonto y simple a puros efectos de ver si andaba. Para eso agregué el archivo mcs/class/System/System.Message/Message.cs con el siguiente código :

#if NET_2_0
using System;
using System.Runtime.CompilerServices;

namespace System.Message
{
  public class Message
  {
    public Message (string p)
    {
      DefinePattern (p);
    }
	
    [MethodImplAttribute (MethodImplOptions.InternalCall)]
    internal extern void DefinePattern (string pattern);
  }
}
#endif

Me queda todavía la duda de la diferencia exacta de éste método y el explicado en el artículo, ya que yo estoy declarando mi método como “internal extern” y no solamente “extern”. El “internal” salió por prueba y error mirando métodos de otras clases y ya veré de preguntar cuál es la diferencia exacta entre ambos casos.

Una vez compilada e instalada la nueva VM y compilado el siguiente ejemplo :

using System.Message;
public class Test
{
  static public void Main(string [] args)
  {
    Message m = new Message("void Test* (*)");
    System.Console.WriteLine("Test Done");
  }
}

Obtuve lo esperado y “solo” tuve que pelear 6hs 😀

gazer@Dana:~/src/ejemplos$ mono Test2.exe 
New pattern Defined : void Test* (*)
Test Done