Skip to content

Commit 206e98f

Browse files
committed
Updated README
1 parent d4c0d95 commit 206e98f

1 file changed

Lines changed: 122 additions & 84 deletions

File tree

README.md

Lines changed: 122 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,44 @@
11
# MethodProxies
22

3-
MethodProxies is a Pharo instrumentation library that implements message-passing control (it controls method execution). It features a stratified architecture that cleanly separates the instrumentation mechanism from the user-defined controlling methods.
4-
Users simply need to **subclass a handler class** and define the desired controlling methods.
5-
The library ensures that these methods execute safely, as concerns such as meta-safety and stack unwinding are managed by the framework. Its robust design allows MethodProxies to instrument any method safely.
3+
MethodProxies is a Pharo instrumentation library for **message-passing control**. It lets you execute custom code **before**, **after**, **instead of**, or **during the unwind of** any method execution — without changing the method's source.
64

7-
Message-passing control is an instrumentation paradigm where programs are instrumented to perform actions before, instead, and after a message is sent. This replaces the original method entirely, or performs no additional action at all.
5+
It is designed for building profilers, tracers, call-graph analyzers, mocks, and other dynamic analysis tools. Its stratified architecture cleanly separates the instrumentation mechanism (handled by the framework) from the user-defined behavior, so all you need to do is **subclass a handler** and define a few hooks.
86

9-
## How to load
7+
MethodProxies guarantees:
8+
9+
- **Meta-safety** — instrumented methods can safely call other instrumented methods without triggering infinite recursion.
10+
- **Unwind-safety** — non-local returns and exceptions are handled correctly.
11+
- **Meta-thread safety** — the meta-level state is tracked per process.
12+
- **Dynamic (de)instrumentation** — instrumentation can be installed and removed at run time.
13+
- **Practical overhead** — the trap-method approach integrates with the JIT and with polymorphic inline caches.
14+
15+
## Loading
1016

1117
```st
1218
EpMonitor disableDuring: [
13-
Metacello new
14-
baseline: 'MethodProxies';
15-
repository: 'github://pharo-contributions/MethodProxies/src';
16-
load. ]
19+
Metacello new
20+
baseline: 'MethodProxies';
21+
repository: 'github://pharo-contributions/MethodProxies/src';
22+
load ]
1723
```
1824

19-
#### How to depend on this project
25+
To depend on it from your own baseline:
2026

2127
```st
22-
spec
23-
baseline: 'MethodProxies'
24-
with: [ spec repository: 'github://pharo-contributions/MethodProxies/src' ].
28+
spec
29+
baseline: 'MethodProxies'
30+
with: [ spec repository: 'github://pharo-contributions/MethodProxies/src' ]
2531
```
2632

27-
## Examples
33+
## Basic Usage
2834

29-
A simple counting handler.
35+
A proxy associates a method with a handler. You install it, enable instrumentation, run your code, then uninstall.
3036

3137
```st
32-
handler := MpCountingHandler new.
33-
p := MpMethodProxy
34-
onMethod: Object >> #error:
35-
handler: handler.
38+
handler := MpCountingHandler new.
39+
p := MpMethodProxy
40+
onMethod: Object >> #error:
41+
handler: handler.
3642
p install.
3743
p enableInstrumentation.
3844
1 error: 'foo'.
@@ -41,107 +47,139 @@ handler count.
4147
>>> 1
4248
```
4349

44-
A simple example showing that an handler may failed still the system does not destroy Pharo.
50+
A failure inside a handler will not corrupt the system. The framework safely unwinds the stack and restores normal execution:
4551

4652
```st
47-
"Managing exceptions in the spyied method"
48-
49-
p := MpMethodProxy
50-
onMethod: MpClassB >> #methodTwo
51-
handler: MpFailingBeforeHandler new.
53+
p := MpMethodProxy
54+
onMethod: MpClassB >> #methodTwo
55+
handler: MpFailingBeforeHandler new.
5256
p install.
5357
p enableInstrumentation.
5458
MpClassB new methodTwo.
5559
p uninstall.
5660
```
5761

58-
### Sharing Handlers
62+
## Defining a Handler
63+
64+
Subclass `MpHandler` and override one or more of the hooks below.
5965

60-
The design of method proxies supports the sharing of handlers between multiple proxied methods.
61-
For example the following example shows how we can monitor and gather information about `new` and `new:` in the same place
66+
### `before` and `after`
6267

6368
```st
64-
h := MpAllocationProfilerHandler new.
65-
p1 := MpMethodProxy
66-
onMethod: Behavior >> #basicNew
67-
handler: h.
68-
p2 := MpMethodProxy
69-
onMethod: Object >> #clone
70-
handler: h.
71-
72-
p1 install.
73-
p2 install.
74-
p1 enableInstrumentation.
75-
p2 enableInstrumentation.
69+
beforeExecutionWithReceiver: receiver arguments: args
70+
"Called before the controlled method runs."
7671
77-
Object new clone.
72+
afterExecutionWithReceiver: receiver arguments: args returnValue: value
73+
"Called after a normal return. MUST return the value the proxied method should yield.
74+
Return `value` to keep it as-is, or return something else to override the result."
75+
```
7876

79-
p1 uninstall.
80-
p2 uninstall.
77+
For convenience, two simpler hooks are also provided. They are useful when the receiver, the arguments, or the return value are not needed:
8178

82-
h allocations size
83-
>>> 2
79+
```st
80+
beforeMethod "no arguments"
81+
afterMethod "no arguments, cannot modify the return value"
8482
```
8583

86-
### Automatically proxies propagation (virus)
84+
Example — a handler that rewrites the return value (defined on a subclass `MpChangesReturnValueHandler` of `MpHandler`):
85+
86+
```st
87+
MpChangesReturnValueHandler >>
88+
afterExecutionWithReceiver: receiver arguments: arguments returnValue: returnValue
89+
^ 'trapped [' , returnValue asString , ']'
90+
```
8791

88-
A more advanced and challenging for the architecture of method proxies is a propagating handler.
89-
The idea is simple, before a method executes, all the implementor methods of its messages are proxified with propagating handlers.
90-
So instead of proxifying the complete system only we subpart is spyied. This is this challenging because the infrastructure should make sure that we are not proxifying the code that is proxifying the system else we would end up in a severe and endless loop.
91-
The handler design protects you from that by avoiding that you extend and break the clear separation between base and meta-level.
92+
### `instead` — replace the original method
93+
94+
The `instead` hook completely replaces the original method body. When `instead` is defined, `before` and `after` are **not** invoked: the original method is never called.
95+
96+
For example, a handler `MpAlwaysReturn42Handler` (a subclass of `MpHandler`) can override the receiver's behavior like this:
9297

9398
```st
94-
testCase := StringTest selector: #testAsCamelCase.
95-
method := StringTest >> #testAsCamelCase.
96-
(MpMethodProxy
97-
onMethod: method
98-
handler: MpProfilingHandler new) install; enableInstrumentation.
99-
testCase run.
99+
MpAlwaysReturn42Handler >> insteadExecutionWithReceiver: receiver
100+
^ 42
101+
```
100102

101-
proxies := MpMethodProxy allInstances.
102-
proxies do: #uninstall.
103+
Using it:
104+
105+
```st
106+
p := MpMethodProxy
107+
onMethod: MpClassA >> #methodOne
108+
handler: MpAlwaysReturn42Handler new.
109+
p install.
110+
p enableInstrumentation.
111+
MpClassA new methodOne.
112+
>>> 42 "the original method is not executed"
113+
p uninstall.
103114
```
104115

105-
## Design
116+
For methods that take arguments, use `insteadExecutionWithReceiver:arguments:` (or one of the keyword variants `insteadExecutionWithReceiver:with:`, `...with:with:`, etc.).
106117

107-
MethodProxies design rests on two pillars: **handlers** and the **trap method** as presented in the Figure below.
118+
The `instead` hook is useful for mocking, stubbing in tests, fault injection, or quickly experimenting with alternative implementations.
108119

109-
- Each controlled method is associated with a dedicated handler that defines the controlling methods. This specialization allows different instrumented methods to execute different handlers: **each controlled method can have its own handler**. Handlers can also be shared.
110-
- The second pillar, the trap method, is a **pre-compiled template method**. Instrumentation is achieved by copying the trap method and applying literal patching to replace the literal references to the actual handler and the original method. Finally, the trap method leverages Pharo’s *stack unwinding* mechanism to guarantee execution of the handlers without allocating block closures, gaining significantly in performance. The trap method is a precompiled template in which the before and after method handlers, and the original method are represented as literals, later updated through literal patching. It ensures safety by including a meta-safe mechanism to prevent infinite recursion and safe stack unwinds.
120+
### Unwind — non-local returns and exceptions
111121

112-
![UML](https://github.com/user-attachments/assets/c617f480-702d-49d3-8e33-c1aec0756258)
122+
`aboutToReturnWithReceiver:arguments:` is invoked when the controlled method exits via a stack unwind — an exception or a non-local return. By default it delegates to `afterExecutionWithReceiver:arguments:returnValue:` with a `nil` return value, but you can override it to react specifically to abnormal exits:
113123

114-
MethodProxies has a stratified architecture structured around two core classes: `MpMethodProxy` and `MpHandler`:
124+
```st
125+
MyHandler >> aboutToReturnWithReceiver: receiver arguments: args
126+
"Called only when the method is being unwound."
127+
...
128+
```
115129

116-
- `MpMethodProxy` manages the lifecycle of an instrumented method. Users are not exposed to the internal logic and the implementation details.
117-
- `MpHandler` is the root of handlers. Users can subclass it and define the controlling methods.
130+
### Sharing handlers
118131

119-
Its API is composed of three methods:
132+
The same handler can be installed on multiple methods, which is convenient for aggregating information from several places. For example, monitoring `basicNew` and `clone` together:
120133

121-
- `beforeExecutionWithReceiver:arguments:` is called before the controlled method is invoked. It receives as arguments the actual receiver and the arguments of the controlled message send.
122-
- `aboutToReturnWithReceiver:arguments:` is called before the controlled method exits due to a stack unwind. This situation occurs in the presence of non-local returns or exceptions.
123-
- `afterExecutionWithReceiver:arguments:returnValue:` is called after the controlled method returns. This hook allows executing actions after the method has run, inspecting or modifying the return value. The return value of the controlled call is passed as an argument, and the method must return the final value to be used as the result of the instrumented method.
134+
```st
135+
h := MpAllocationProfilerHandler new.
136+
p1 := MpMethodProxy onMethod: Behavior >> #basicNew handler: h.
137+
p2 := MpMethodProxy onMethod: Object >> #clone handler: h.
124138
125-
Moreover, two higher-level hooks are provided, defined in terms of the ones described above.
126-
- `beforeMethod` is invoked before the method execution begins.
127-
- It is a simpler version of `beforeExecutionWithReceiver:arguments:` that does not receives any arguments.
128-
- `afterMethod` is invoked before the method returns, either by normal completion or due to a stack unwind. This method does not receives any arguments and it does not allow modification of the return value.
139+
p1 install. p2 install.
140+
p1 enableInstrumentation. p2 enableInstrumentation.
129141
130-
![Instrumentation](https://github.com/user-attachments/assets/6c9a0f6a-011e-49f9-a196-3d2ef5c1d5d7)
142+
Object new clone.
131143
132-
## Some Archeology and History
144+
p1 uninstall. p2 uninstall.
145+
h allocations size
146+
>>> 2
147+
```
133148

134-
Method Wrappers were originally developed by John Brant for proprietary software. You can read "Evaluating Message Passing Control" article from S. Ducasse to understand the original implementation and a comparison with other approaches. What you see is that back in 1998/9 there were already multiple ways to control message passing. Method Wrappers is one of them.
149+
## Design
135150

136-
MethodWrappers were basically using CompiledMethod prototypes that were patched (cloned + adding selector/class) during their application because the VM of the system where Method Wrappers were implemented did not support to have anything but CompiledMethod as value of method dictionaries.
137-
This is not the case for Pharo. And the design and implementation of MethodProxies allows us to wrap any part of the system and in addition to design propagating proxies without blowing up the system. This is a key properties for us.
151+
MethodProxies rests on two pillars: **handlers** and the **trap method**.
138152

139-
**Note for grumpies.** For the people that could think that we would like to steal this idea, we encourage them to lower their paranoia by having a look at our CV and publication record list. We redeveloped from scratch this library while taking into account the original intention and we wrote many tests to validate them.
153+
- Each instrumented method is associated with a handler. Users subclass `MpHandler` and override the hooks; they never touch the low-level machinery. Different methods can have different handlers, and handlers can be shared.
154+
- The **trap method** is a precompiled template installed in place of the instrumented method. At installation time it is patched via *literal patching* to reference the actual handler and the original method. The original method is kept in the same method dictionary under a hidden selector, so the forwarding call becomes a monomorphic, JIT-friendly message send.
140155

141-
## Bibliography
156+
The trap also tracks a meta-level state on the active process. Whenever a hook executes, the process is marked as *meta*. While in this state, any instrumented method called from within the handler short-circuits to the original behavior, which avoids the infinite-recursion problem that affects simpler approaches. Stack unwinds are handled directly through the VM's unwind mechanism, **without allocating block closures**, giving significantly better performance than approaches based on `ensure:`.
142157

143-
- Jordan Montaño S., Sandoval Alcócer J., Polito G., Ducasse S., Tesone P., MethodProxies: A Safe and Fast Message-Passing Control Library, IWST '24 June 2024, France. [PDF](https://hal.science/hal-04708729v1/document)
144-
- Stéphane Ducasse, Evaluating Message Passing Control Techniques in Smalltalk, Journal of Object-Oriented Programming (JOOP), 12, 39–44, SIGS Press, 1999, Impact factor 0.306. [PDF](http://rmod-files.lille.inria.fr/Team/Texts/Papers/Duca99aMsgPassingControl.pdf)
145-
- John Brant, Brian Foote, Ralph E. Johnson, and Donald Roberts. Wrappers to the Rescue. In Proceedings of European Conference on Object-Oriented Programming (ECOOP). Springer, Berlin, Heidelberg, 1998. http://www.laputan.org/brant/brant.html (https://link.springer.com/chapter/10.1007/BFb0054101)
158+
The architecture has two strata: `MpMethodProxy` manages the lifecycle of an instrumented method and hides the implementation details, while `MpHandler` is the root class users subclass to define their controlling methods.
159+
160+
![Architecture](https://github.com/user-attachments/assets/c617f480-702d-49d3-8e33-c1aec0756258)
161+
162+
![Instrumentation](https://github.com/user-attachments/assets/6c9a0f6a-011e-49f9-a196-3d2ef5c1d5d7)
146163

164+
A full description of the design, the implementation, and the empirical evaluation is available in the IWST '24 paper listed in the bibliography. On a suite of 48 benchmarks across four real-world applications, the average overhead is about 1.54× and the meta-safety mechanism itself adds roughly 9% on top. Compared to instrumentation based on the `run:with:in:` hook — which cannot be JIT-compiled — MethodProxies is on average 6.5× faster, with peaks up to 40×.
165+
166+
## Advanced: Propagating Handlers
167+
168+
A more advanced use case is a *propagating* handler: before a method runs, all the implementors of the messages it sends are themselves wrapped with the same kind of handler. Instead of instrumenting the entire system upfront, only the subset of code that actually executes is instrumented. The challenge is to avoid instrumenting the instrumentation code itself, which would lead to an infinite loop. The handler design and the meta-safety mechanism take care of this by preserving the separation between base and meta levels.
169+
170+
```st
171+
testCase := StringTest selector: #testAsCamelCase.
172+
method := StringTest >> #testAsCamelCase.
173+
(MpMethodProxy
174+
onMethod: method
175+
handler: MpProfilingHandler new) install; enableInstrumentation.
176+
testCase run.
177+
178+
proxies := MpMethodProxy allInstances.
179+
proxies do: #uninstall.
180+
```
181+
182+
## Bibliography
147183

184+
- Jordan Montaño S., Sandoval Alcócer J., Polito G., Ducasse S., Tesone P. *MethodProxies: A Safe and Fast Message-Passing Control Library.* IWST '24, June 2024, France. [PDF](https://hal.science/hal-04708729v1/document)
185+
- Stéphane Ducasse. *Evaluating Message Passing Control Techniques in Smalltalk.* Journal of Object-Oriented Programming (JOOP), 12, 39–44, SIGS Press, 1999. [PDF](http://rmod-files.lille.inria.fr/Team/Texts/Papers/Duca99aMsgPassingControl.pdf)

0 commit comments

Comments
 (0)