364. Implementing an interception framework - Compile-time IL weaving

בהמשך לטיפים הקודמים שהיו לפני כמה זמן, נראה עכשיו עוד שיטה ליצור Interception לאובייקטים.

ממליץ לקרוא את הטיפים הקודמים כדי להזכר (350-362)

השיטה הפעם נקראת Compile-time IL weaving.

מה זה?

הרעיון הוא הרעיון המטורף הבא:

אחרי שאנחנו מקמפלים אפליקציה, נוצרים לנו DLLים המכילים את הקוד של האפליקציה שמקומפל לשפת ביניים (מה שנקרא CIL או MSIL, תלוי מתי שאלתם).

Compile-time IL weaving מציע לקחת את הDLL שנוצר, ולשנות את הIL שנוצר, כך שיבצע התנהגות שונה, ועל ידי כך להשיג את הInterception.

הIL לאחר השינוי נשמר בDLL, כך שהשינוי הוא שינוי סטטי. (לא מתבצע בזמן ריצה)


אז איך עושים את זה? יש מספר APIים בעולם המאפשרים לפרסר IL ולבצע עליו מניפולציות, ביניהם: Mono.Cecil, Microsoft.CCI, PostSharp API.

בד”כ לא נרצה להשתמש בהם ישירות מאחר וזו משימה יחסית מסובכת לערוך את הIL בלי לעשות נזק, וגם לדאוג שמה שרצינו לעשות יעבוד.

למרות שלפעמים יחסית קל להשתמש בהם כדי לעשות דברים פשוטים, למשל לשנות את הProperties של המחלקות שלנו שיקראו לPropertyChanged של INotifyPropertyChanged (יש דוגמאות באינטרנט לדבר הזה)

במידה ונעשה משהו כזה, האידיאל יהיה שיהיה איזשהו Post build event בSolution שלנו שמריץ את השינוי של הIL על הDLLים המקומפלים.


קיימים Frameworkים יחסית מפותחים, ביניהם AfterThought וPostSharp המאפשרים לנו לבצע את הפעילות הזו בזמן קימפול בצורה נוחה.

PostSharp הוא הFramework המוביל בשוק בתחום הזה ומה שהוא נותן זה מימוש לAOP (ראו גם טיפ מספר 358) באמצעות Compile-time IL weaving:

איך זה נראה? אנחנו צריכים לרשת מAttributeים של התשתית, למשל

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[Serializable]
public class LogAttribute : OnMethodBoundaryAspect
{
private static readonly ILog mLog = LogManager.GetLogger(typeof (LogAttribute));
public override void OnEntry(MethodExecutionArgs args)
{
mLog.DebugFormat("Entering {0}, with parameters {1}",
args.Method,
GetParameters(args));
}
public override void OnExit(MethodExecutionArgs args)
{
mLog.DebugFormat("Exiting {0} with return value: {1}",
args.Method,
args.ReturnValue);
}
public override void OnException(MethodExecutionArgs args)
{
mLog.Error("An error occured on " + args.Method, args.Exception);
}
private static string GetParameters(MethodExecutionArgs args)
{
return string.Join
(",",
args.Method.GetParameters().Select((x, i) =>
new {x.Name, Index = i}).
Select(x => x.Name + ":" + args.Arguments[x.Index]).ToArray());
}
}

ואז לשים אותם מעל הפונקציות שלנו, למשל:

1
2
3
4
5
6
7
8
9
10
11
12
[Log]
public static int Factorial(int n)
{
int result = 1;
for (int i = 1; i <= n; i++)
{
result *= i;
}
return result;
}

בפועל יקרא הקוד שלנו. בואו נסתכל על הקוד שיוצא בReflector:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static int Factorial(int n)
{
int returnValue;
Arguments<int> argsCollection = new Arguments<int>();
argsCollection.Arg0 = n;
MethodExecutionArgs args = new MethodExecutionArgs(null, argsCollection);
args.Method = mAspects.m1;
mAspects.a0.OnEntry(args);
if (args.FlowBehavior == FlowBehavior.Return)
{
return (int)args.ReturnValue;
}
try
{
int result = 1;
for (int i = 1; i <= n; i++)
{
result *= i;
}
int temp = result;
returnValue = temp;
}
catch (Exception exception)
{
args.Exception = exception;
mAspects.a0.OnException(args);
throw;
}
finally
{
args.ReturnValue = returnValue;
mAspects.a0.OnExit(args);
}
return returnValue;
}

מה שאנחנו רואים פה זה שנקרא שלפני ביצוע הקוד שלנו, מתבצעת קריאה לOnEntry. כאשר נתפס Exception, מתבצעת קריאה לOnException, וביציאה למתודה מתבצעת קריאה לOnExit.

מה שמגניב זה שיש Member בשם mAspects (זה לא באמת השם שלו, אבל לצורך העניין) שמכיל את האובייקטים של הAttributeים והמתודות, והוא נוצר בגישה הראשונה של המחלקה שלנו, כך שבעצם מתבצעת קריאה ישירות למתודה (ולא ע"י עטיפה כמו שראינו בדוגמאות של הRuntime Subclassing), כך שהקוד שנוצר הוא ממש מהיר.

מה שכן, צריך לשים לב לדברים הבאים:

  • PostSharp, הכלי המוביל בשוק, הוא לא חינמי ועולה קצת כסף. שאר הכלים פחות טובים
  • הדבר הזה קצת מוזר בדיבוג – אנחנו נראה שאנחנו נכנסים לקוד שבAttribute בכניסה למתודה, למרות שלא ראינו בקוד קריאה אליו.

למרות זאת, מדובר בכלי מאוד חזק שיכול להיות מאוד שימושי לכתיבת תשתיות.

המשך יום מקומפל ארוג לטובה.

שתף