Skip to content

Commit 9151f57

Browse files
Document lifetimes restrictions
1 parent f50c644 commit 9151f57

File tree

1 file changed

+97
-0
lines changed

1 file changed

+97
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,100 @@
11
# Lifetimes
22

33
Emacs internals lifetimes.
4+
5+
## Environment Lifetime
6+
7+
Environment lives as long as the function it was given to executes. This covers both module initialization and Lisp functions defined in Swift. Using environment outside of its lifetime will most likely crash Emacs. You can think of ``Environment`` to be always an unowned reference.
8+
9+
This means that storing ``Environment`` as part of some state, or capturing it in a closure that will be used afterwards is not going to work. The most typical problem looks somwehat like this:
10+
```swift
11+
try env.defun("test") {
12+
// some code before
13+
try env.funcall("lisp-function")
14+
// some code after
15+
}
16+
```
17+
When the function does a lot of things, it's hard to spot the fact that we are actually using the wrong environment. We captured the one from the outer scope, and using it here will cause Emacs to crash. Instead, we can explicitly ask for a new instance of ``Environment`` in every Swift-defined Lisp function.
18+
```swift
19+
try env.defun("test") {
20+
(env: Environment) in
21+
// some code before
22+
try env.funcall("lisp-function")
23+
// some code after
24+
}
25+
```
26+
This code doesn't change the number of required arguments to call `test`, Emacs already passes a new environemnt with every function invocation. This way we just ask ``Environment`` to pass it to us on call.
27+
28+
This lifetime restriction ensures one of the core principles of Emacs dynamic modules **"Emacs calls into then module's code when it wants to, not the other way around"**. Using ``Environment`` outside of its lifetime means calling into Emacs asynchronously when Emacs does not expect it to happen. That will violate its concurrency model.
29+
30+
## EmacsValue Lifetime
31+
32+
Similarly to ``Environment``, opaque ``EmacsValue`` also has a limited lifetime. It is not enforced as strictly, and can produce even more confusion.
33+
34+
Essentially every ``EmacsValue`` is bound to the ``Environment`` instance that produced it. This means that ``EmacsValue`` has *the same lifetime* as its ``Environment``. In the most probable scenario, you produce a value and use it with the same environment. Nothing to worry about in this case!
35+
```swift
36+
let value = try env.funcall("foo")
37+
try env.funcall("bar", with: value)
38+
```
39+
40+
The problem comes when you want to keep certain value and share it between two environments.
41+
```swift
42+
var stash: EmacsValue = env.Nil
43+
try env.defun("stash-arg") {
44+
(arg: EmacsValue) in stash = arg
45+
}
46+
try env.defun("get-stash") {
47+
stash
48+
}
49+
```
50+
It should not work because of the lifetimes violation, but in most cases it does. On my machine, this code works as the user expected it to work. However, if we modify it a little bit, we can receive some very confusing results.
51+
52+
```swift
53+
var stash = [EmacsValue]()
54+
try env.defun("stash-arg") {
55+
(arg: EmacsValue) in stash.append(arg)
56+
}
57+
try env.defun("get-stash") {
58+
stash
59+
}
60+
```
61+
Looks very much the same, we keep all the arguments instead of the last one. So, what would be the problem?
62+
```emacs-lisp
63+
(stash-arg 1)
64+
(stash-arg 2)
65+
(stash-arg 3)
66+
(get-stash) ;; => [3 3 3]
67+
```
68+
It returns a vector of `3`s! The size is right, the last value is right, but the whole vector shares the same value. It is especially strange after the previous example. The reason is the lifetime of `arg`, in this situation it is not enforced. However, Emacs reuses the same memory to store a new argument value every time. It is just an implementation detail of Emacs that we discovered accidentally. We should *never* rely on such undocumented features. They can change from one release to another, and behave differently on different platforms.
69+
70+
Instead, we should use ``PersistentEmacsValue``.
71+
```swift
72+
var stash = [EmacsValue]()
73+
try env.defun("stash-arg") {
74+
(arg: PersistentEmacsValue) in stash.append(arg)
75+
}
76+
try env.defun("get-stash") {
77+
stash
78+
}
79+
```
80+
This fixes it! We just changed parameter type of our function and that's enough! This way we tell `EmacsSwiftModule` that actually the Swift side should take care of this value's lifetime. ``PersistentEmacsValue`` effectively marries Swift's ARC and Emacs' garbage collection, so we can share values across environments.
81+
82+
``PersistentEmacsValue`` also works with `funcall` and `apply` result type inferrence, so you can write:
83+
```swift
84+
let x: PersistentEmacsValue = try env.funcall("foo")
85+
let y = try env.funcall("bar") as PersistentEmacsValue
86+
```
87+
88+
If you already have ``EmacsValue``, you can turn it into ``PersistentEmacsValue`` by calling ``Environment/preserve(_:)``.
89+
```swift
90+
let lambda = try env.preserve(env.defun {
91+
// do some cool stuff
92+
})
93+
```
94+
After preservation, `lambda` can be safely used from different functions.
95+
96+
> Info: ``Environment`` also provides ``Environment/retain(_:)`` and ``Environment/release(_:)`` low-level APIs for manual reference-counting. But ``PersistentEmacsValue`` and ``Environment/preserve(_:)`` should be preferred to avoid mistakes.
97+
98+
## Concurrency
99+
100+
As it was mentioned earlier, ``Environment`` lifetime restriction comes from the desire to keep Emacs own concurrency model intact. This also includes another rule for using ``Environment`` - it should be used on the same thread it was created on. It is important to keep it mind, even considering that using ``Environment`` asynchronously will most likely violate its lifetime. To learn how to mix asynchronous code with Emacs interactions, please refer to <doc:AsyncCallbacks>.

0 commit comments

Comments
 (0)