Skip to content

Commit f947910

Browse files
committed
Add new docs abbout assigning variables to global scope
1 parent a7bf9f9 commit f947910

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

docs/assigning-global-variables.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
title: Setting global variables or functions
3+
---
4+
5+
import { SideBySide } from "@site/src/components/SideBySide";
6+
7+
In some Lua environments, the host application expects you to define some global variables or functions as part of the API. For example, some engines might allow you to define some event handlers in your Lua, that will be called by the engine when different events happen:
8+
9+
```lua title=example.lua
10+
function OnStart()
11+
-- start event handling code
12+
end
13+
14+
function OnStateChange(newState)
15+
-- state change event handler code
16+
end
17+
```
18+
19+
Due to the way TSTL translates module code, functions will be `local` after translation, causing the engine not to find them:
20+
21+
<SideBySide>
22+
23+
```typescript title=input.ts
24+
function OnStart(this: void) {
25+
// start event handling code
26+
}
27+
function OnStateChange(this: void, newState: State) {
28+
// state change event handler code
29+
}
30+
```
31+
32+
```lua title=output.lua
33+
local function OnStart()
34+
end
35+
local function OnStateChange(newState)
36+
end
37+
```
38+
39+
</SideBySide>
40+
41+
This means we need some extra helper code to correctly register these global variables so your environment can access them.
42+
43+
## Setting global variables with a helper function
44+
45+
One way to assign global variables and functions is to use a helper function like this:
46+
47+
```typescript
48+
function registerEventHandler<TArgs extends unknown[]>(
49+
handlerName: string,
50+
handler: (this: void, ...args: TArgs) => void,
51+
): void {
52+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
53+
globalThis[handlerName] = handler;
54+
}
55+
```
56+
57+
This helper function can be added in some shared TypeScript helper file and imported wherever you need it.
58+
59+
You can now write the example code like this:
60+
61+
```typescript
62+
registerEventHandler("OnStart", () => {
63+
// start event handling code
64+
});
65+
registerEventHandler("OnStateChanged", (newState: State) => {
66+
// state change event handler code
67+
});
68+
```
69+
70+
Of course you can modify `registerEventHandler` to your needs if you need to assign variables of different types to the global scope. For example, you could add a second `register` function for assigning non-function values if needed:
71+
72+
```typescript
73+
function registerGlobalVariable<T>(variableName: string, value: T): void {
74+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
75+
globalThis[handlerName] = value;
76+
}
77+
```
78+
79+
## Registering functions as class methods with a decorator
80+
81+
Sometimes you don't want to register just a loose function, but instead register a class or class method. A very nice way to do this is to use decorators (they unfortunately only work on classes, and not for loose functions).
82+
83+
One example of such a decorator is:
84+
85+
```typescript
86+
function registerEventHandler<TReturn, TArgs extends unknown[]>(
87+
method: (...args: TArgs) => TReturn,
88+
context: ClassMethodDecoratorContext,
89+
) {
90+
/** @noSelf - the engine will not pass self parameter so wrap in lambda without self */
91+
const contextless = (...args: TArgs) => method(...args);
92+
// We can read the name of the method from the context
93+
const globalName = context.name;
94+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
95+
globalThis[globalName] = contextless;
96+
}
97+
```
98+
99+
You can then write above example as:
100+
101+
```typescript
102+
class EventHandlers {
103+
@registerEventHandler
104+
public OnStart() {
105+
// start event handling code
106+
}
107+
@registerEventHandler
108+
public OnStateChanged(newState: State) {
109+
// state change event handler code
110+
}
111+
}
112+
```
113+
114+
:::note
115+
In the above example, `this` will be `nil` in the methods, do not try to use other members in the EventHandlers class!
116+
:::
117+
118+
## Registering classes with a decorator
119+
120+
Sometimes you want to register classes as globals, you can also do that with a decorator:
121+
122+
```typescript
123+
function registerClass<TClass, TArgs extends unknown[]>(
124+
c: new (...args: TArgs) => TClass,
125+
context: ClassDecoratorContext,
126+
) {
127+
if (context.name) {
128+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
129+
globalThis[context.name] = c;
130+
}
131+
}
132+
```
133+
134+
You can now register any class by simply adding the decorator:
135+
136+
```typescript
137+
@registerClass
138+
class EventHandlers {
139+
public OnStart() {
140+
// start event handling code
141+
}
142+
public OnStateChanged(newState: State) {
143+
// state change event handler code
144+
}
145+
}
146+
```
147+
148+
### Custom global name decorator
149+
150+
In the examples above, the decorators directly used the name of the decorated class or method, but with decorator parameters you can also specify custom override names:
151+
152+
```typescript
153+
const registerClass =
154+
(globalName: string) =>
155+
<TClass, TArgs extends unknown[]>(c: new (...args: TArgs) => TClass, context: ClassDecoratorContext) => {
156+
if (context.name) {
157+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
158+
globalThis[globalName] = c;
159+
}
160+
};
161+
```
162+
163+
Now instead of taking the global name from the class, you a custom name yourself:
164+
165+
```typescript
166+
@registerClass("CustomGlobalName")
167+
class EventHandlers {
168+
public OnStart() {
169+
// start event handling code
170+
}
171+
public OnStateChanged(newState: State) {
172+
// state change event handler code
173+
}
174+
}
175+
```
176+
177+
## Assigning to globals with declarations
178+
179+
The main weakness of the above methods is that you can declare any string, not protecting you from typos, and not giving any kind of editor support. This is fine if there are only a few such registrations that need to be done, but is somewhat error prone.
180+
181+
An alternative method would be to explicitly declare the global variables in a declarations file:
182+
183+
```ts
184+
declare var OnStart: (this: void) => void;
185+
declare var OnStateChanged: (this: void, newState: State) => void;
186+
```
187+
188+
You can then assign to these functions as if they were global variables:
189+
190+
```typescript
191+
OnStart = () => {
192+
// start event handling code
193+
};
194+
OnStateChanged = (newState: State) => {
195+
// state change event handler code
196+
};
197+
```
198+
199+
This of course only works if you know the names of the global variables beforehand, if these names are dynamic, consider using one of the other methods instead.

sidebars.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"caveats",
1010
"the-self-parameter",
1111
"advanced/writing-declarations",
12+
"assigning-global-variables",
1213
"external-code",
1314
"publishing-modules",
1415
"editor-support"

0 commit comments

Comments
 (0)