From f94791014b62e97d900d1312ed2883edaa17fbc0 Mon Sep 17 00:00:00 2001 From: Perryvw Date: Sat, 13 Jul 2024 17:21:58 +0200 Subject: [PATCH 1/3] Add new docs abbout assigning variables to global scope --- docs/assigning-global-variables.md | 199 +++++++++++++++++++++++++++++ sidebars.json | 1 + 2 files changed, 200 insertions(+) create mode 100644 docs/assigning-global-variables.md diff --git a/docs/assigning-global-variables.md b/docs/assigning-global-variables.md new file mode 100644 index 0000000..c9b8afd --- /dev/null +++ b/docs/assigning-global-variables.md @@ -0,0 +1,199 @@ +--- +title: Setting global variables or functions +--- + +import { SideBySide } from "@site/src/components/SideBySide"; + +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: + +```lua title=example.lua +function OnStart() + -- start event handling code +end + +function OnStateChange(newState) + -- state change event handler code +end +``` + +Due to the way TSTL translates module code, functions will be `local` after translation, causing the engine not to find them: + + + +```typescript title=input.ts +function OnStart(this: void) { + // start event handling code +} +function OnStateChange(this: void, newState: State) { + // state change event handler code +} +``` + +```lua title=output.lua +local function OnStart() +end +local function OnStateChange(newState) +end +``` + + + +This means we need some extra helper code to correctly register these global variables so your environment can access them. + +## Setting global variables with a helper function + +One way to assign global variables and functions is to use a helper function like this: + +```typescript +function registerEventHandler( + handlerName: string, + handler: (this: void, ...args: TArgs) => void, +): void { + // @ts-ignore tell TS to ignore us 'illegally' writing to global scope + globalThis[handlerName] = handler; +} +``` + +This helper function can be added in some shared TypeScript helper file and imported wherever you need it. + +You can now write the example code like this: + +```typescript +registerEventHandler("OnStart", () => { + // start event handling code +}); +registerEventHandler("OnStateChanged", (newState: State) => { + // state change event handler code +}); +``` + +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: + +```typescript +function registerGlobalVariable(variableName: string, value: T): void { + // @ts-ignore tell TS to ignore us 'illegally' writing to global scope + globalThis[handlerName] = value; +} +``` + +## Registering functions as class methods with a decorator + +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). + +One example of such a decorator is: + +```typescript +function registerEventHandler( + method: (...args: TArgs) => TReturn, + context: ClassMethodDecoratorContext, +) { + /** @noSelf - the engine will not pass self parameter so wrap in lambda without self */ + const contextless = (...args: TArgs) => method(...args); + // We can read the name of the method from the context + const globalName = context.name; + // @ts-ignore tell TS to ignore us 'illegally' writing to global scope + globalThis[globalName] = contextless; +} +``` + +You can then write above example as: + +```typescript +class EventHandlers { + @registerEventHandler + public OnStart() { + // start event handling code + } + @registerEventHandler + public OnStateChanged(newState: State) { + // state change event handler code + } +} +``` + +:::note +In the above example, `this` will be `nil` in the methods, do not try to use other members in the EventHandlers class! +::: + +## Registering classes with a decorator + +Sometimes you want to register classes as globals, you can also do that with a decorator: + +```typescript +function registerClass( + c: new (...args: TArgs) => TClass, + context: ClassDecoratorContext, +) { + if (context.name) { + // @ts-ignore tell TS to ignore us 'illegally' writing to global scope + globalThis[context.name] = c; + } +} +``` + +You can now register any class by simply adding the decorator: + +```typescript +@registerClass +class EventHandlers { + public OnStart() { + // start event handling code + } + public OnStateChanged(newState: State) { + // state change event handler code + } +} +``` + +### Custom global name decorator + +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: + +```typescript +const registerClass = + (globalName: string) => + (c: new (...args: TArgs) => TClass, context: ClassDecoratorContext) => { + if (context.name) { + // @ts-ignore tell TS to ignore us 'illegally' writing to global scope + globalThis[globalName] = c; + } + }; +``` + +Now instead of taking the global name from the class, you a custom name yourself: + +```typescript +@registerClass("CustomGlobalName") +class EventHandlers { + public OnStart() { + // start event handling code + } + public OnStateChanged(newState: State) { + // state change event handler code + } +} +``` + +## Assigning to globals with declarations + +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. + +An alternative method would be to explicitly declare the global variables in a declarations file: + +```ts +declare var OnStart: (this: void) => void; +declare var OnStateChanged: (this: void, newState: State) => void; +``` + +You can then assign to these functions as if they were global variables: + +```typescript +OnStart = () => { + // start event handling code +}; +OnStateChanged = (newState: State) => { + // state change event handler code +}; +``` + +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. diff --git a/sidebars.json b/sidebars.json index ec3a94e..4b0df3b 100644 --- a/sidebars.json +++ b/sidebars.json @@ -9,6 +9,7 @@ "caveats", "the-self-parameter", "advanced/writing-declarations", + "assigning-global-variables", "external-code", "publishing-modules", "editor-support" From 0fff5f4862e4bc93293e89241c24304cef2410d2 Mon Sep 17 00:00:00 2001 From: Perryvw Date: Sat, 13 Jul 2024 18:03:02 +0200 Subject: [PATCH 2/3] fix typo --- docs/assigning-global-variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assigning-global-variables.md b/docs/assigning-global-variables.md index c9b8afd..ef77d3d 100644 --- a/docs/assigning-global-variables.md +++ b/docs/assigning-global-variables.md @@ -160,7 +160,7 @@ const registerClass = }; ``` -Now instead of taking the global name from the class, you a custom name yourself: +Now instead of taking the global name from the class, you can specify a custom name yourself: ```typescript @registerClass("CustomGlobalName") From ed1a3305854d36ec02dca245282142a244451221 Mon Sep 17 00:00:00 2001 From: Perryvw Date: Sun, 14 Jul 2024 17:44:40 +0200 Subject: [PATCH 3/3] review comments --- docs/assigning-global-variables.md | 54 ++++++++++++++---------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/docs/assigning-global-variables.md b/docs/assigning-global-variables.md index ef77d3d..4a16775 100644 --- a/docs/assigning-global-variables.md +++ b/docs/assigning-global-variables.md @@ -4,7 +4,7 @@ title: Setting global variables or functions import { SideBySide } from "@site/src/components/SideBySide"; -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: +In some Lua environments, the host application expects you to define 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: ```lua title=example.lua function OnStart() @@ -38,11 +38,31 @@ end -This means we need some extra helper code to correctly register these global variables so your environment can access them. +This means we need extra helper code to correctly register these global variables so your environment can access them. + +## Assigning to globals with declarations + +The easiest way to set global variables is to first declare them as existing globals, and then assign their values. As an example: + +```ts +declare var OnStart: (this: void) => void; +declare var OnStateChanged: (this: void, newState: State) => void; + +OnStart = () => { + // start event handling code +}; +OnStateChanged = (newState: State) => { + // state change event handler code +}; +``` + +In the example above, the declarations are in the same file as the value assignments. Alternatively, you could choose to put them in a .d.ts file included in your project. If these globals have pre-defined names specified by the engine the API, it is also possible to include these declarations in the .d.ts files (or types package) for this environment. ## Setting global variables with a helper function -One way to assign global variables and functions is to use a helper function like this: +Another way to assign global variables and functions is to use a helper function. The benefit is that you can strictly type the helper functions to ensure correct types are assigned, as well as having the ability to do additional logic like wrapping or modifying the value. + +The simplest helper function looks like this: ```typescript function registerEventHandler( @@ -78,7 +98,7 @@ function registerGlobalVariable(variableName: string, value: T): void { ## Registering functions as class methods with a decorator -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). +Sometimes you don't want to register just a loose function, but instead register a class or class method. A nice way to do this is to use decorators (they unfortunately only work on classes, and not for loose functions). One example of such a decorator is: @@ -117,7 +137,7 @@ In the above example, `this` will be `nil` in the methods, do not try to use oth ## Registering classes with a decorator -Sometimes you want to register classes as globals, you can also do that with a decorator: +Sometimes you want to register classes instead of functions, you can also do that with a decorator: ```typescript function registerClass( @@ -173,27 +193,3 @@ class EventHandlers { } } ``` - -## Assigning to globals with declarations - -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. - -An alternative method would be to explicitly declare the global variables in a declarations file: - -```ts -declare var OnStart: (this: void) => void; -declare var OnStateChanged: (this: void, newState: State) => void; -``` - -You can then assign to these functions as if they were global variables: - -```typescript -OnStart = () => { - // start event handling code -}; -OnStateChanged = (newState: State) => { - // state change event handler code -}; -``` - -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.