|
5 | 5 | </p> |
6 | 6 |
|
7 | 7 | <p align="center"> |
| 8 | + <img alt="Version" src="https://img.shields.io/badge/version-26.1-blue"> |
8 | 9 | <img alt="Kotlin" src="https://img.shields.io/badge/Kotlin-2.4.10-7F52FF?logo=kotlin&logoColor=white"> |
9 | 10 | <img alt="Build" src="https://img.shields.io/badge/build-Gradle-02303A?logo=gradle&logoColor=white"> |
10 | 11 | <img alt="JVM" src="https://img.shields.io/badge/JVM-17+-ED8B00?logo=openjdk&logoColor=white"> |
@@ -49,41 +50,270 @@ Artifacts are published to [repo.blueva.net](https://repo.blueva.net/releases), |
49 | 50 | **Gradle (Kotlin DSL)** |
50 | 51 |
|
51 | 52 | ```kotlin |
52 | | -repositories { |
53 | | - maven("https://repo.blueva.net/releases") |
| 53 | +plugins { |
| 54 | + java |
| 55 | + id("net.blueva.mawu") version "26.1" |
54 | 56 | } |
55 | 57 |
|
56 | | -plugins { |
57 | | - id("net.blueva.mawu") version "<version>" |
| 58 | +repositories { |
| 59 | + maven("https://repo.blueva.net/releases") |
58 | 60 | } |
59 | 61 | ``` |
60 | 62 |
|
61 | | -Lua sources are then picked up from: |
| 63 | +Every source set gains a Lua directory, so sources are picked up from: |
62 | 64 |
|
63 | 65 | ```text |
64 | 66 | src/ |
65 | 67 | main/ |
66 | 68 | kotlin/ |
67 | 69 | lua/ <- your .lua files |
68 | 70 | resources/ |
| 71 | + test/ |
| 72 | + lua/ <- compiled by compileTestLua |
| 73 | +``` |
| 74 | + |
| 75 | +The plugin puts `net.blueva:mawu-runtime` on the project's implementation |
| 76 | +classpath, which brings `net.blueva:luak-jvm` with it. Building runs |
| 77 | +`compileLua`, which writes one prototype per script into the jar, and |
| 78 | +`compileTestLua` for the test source set. Only the scripts that changed are |
| 79 | +compiled again. |
| 80 | + |
| 81 | +To read Lua from somewhere else as well: |
| 82 | + |
| 83 | +```kotlin |
| 84 | +import net.blueva.mawu.gradle.lua |
| 85 | + |
| 86 | +sourceSets { |
| 87 | + main { |
| 88 | + lua { |
| 89 | + srcDir("scripts") |
| 90 | + } |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +## What the Build Checks |
| 96 | + |
| 97 | +`compileLua` parses every script, so the build fails on anything Lua itself |
| 98 | +refuses to compile, at the file and line that caused it: |
| 99 | + |
| 100 | +```text |
| 101 | +> Lua compilation failed in 2 sources: |
| 102 | + /project/src/main/lua/first.lua:1: syntax error near 'is' |
| 103 | + /project/src/main/lua/second.lua:4: 'end' expected (to close 'function' at line 1) near <eof> |
| 104 | +``` |
| 105 | + |
| 106 | +Every source is checked before the build gives up, so one broken file does not |
| 107 | +hide the next. What is caught is what a Lua compiler catches: syntax, unclosed |
| 108 | +blocks and strings, `goto` and `break` with no label to reach, assignment to a |
| 109 | +`<const>`, and the compiler's own limits. |
| 110 | + |
| 111 | +Two more checks read the compiled form rather than the source, so they cost a |
| 112 | +walk over instructions and no second parser. **Both fail the build**, because a |
| 113 | +name that is wrong is wrong whether Lua notices it now or three frames into a |
| 114 | +run: |
| 115 | + |
| 116 | +- **Undefined globals.** A global a script reads that no script writes, that |
| 117 | + the standard library does not have, and that the build was not told about. |
| 118 | +- **Names off the classpath.** `import 'a.b.C'` and `java.util.logging.Logger` |
| 119 | + are constants in the bytecode, so the class or package they name is looked |
| 120 | + up while it is still just a string. The same walk follows what a call |
| 121 | + returned, so a method missing from the object in hand is caught too. |
| 122 | + |
| 123 | +```text |
| 124 | +> Undefined globals: |
| 125 | + /project/src/main/lua/main.lua:12: undefined global 'confguration' |
| 126 | +
|
| 127 | +> Unknown classes: |
| 128 | + /project/src/main/lua/main.lua:2: no class or package named 'java.util.logging.Loggerr' |
| 129 | + /project/src/main/lua/main.lua:7: 'java.util.logging.Logger' has no member 'infoo' |
69 | 130 | ``` |
70 | 131 |
|
| 132 | +Both take `MawuSeverity.WARN` to report without failing, or `IGNORE` to skip: |
| 133 | + |
| 134 | +```kotlin |
| 135 | +mawu { |
| 136 | + // Globals your host installs before running a script. |
| 137 | + knownGlobals.addAll("housing", "player") |
| 138 | + |
| 139 | + undefinedGlobals.set(MawuSeverity.WARN) |
| 140 | + unknownClasses.set(MawuSeverity.WARN) |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +A value that crosses into Lua as a string or a number stops being a Java object |
| 145 | +there, so `logger:getName():upper()` is Lua's string library and is left alone. |
| 146 | +A name built at runtime is not a constant and cannot be checked at all, which |
| 147 | +is the case the severities exist for. |
| 148 | + |
| 149 | +What stays unchecked is what only running settles: a function called with the |
| 150 | +wrong number of arguments, a method a Lua table does not have, a field of a |
| 151 | +value that turns out to be `nil`. Those surface when the line runs, the same |
| 152 | +way they do in Lua itself. |
| 153 | + |
| 154 | +## Configuration |
| 155 | + |
| 156 | +```kotlin |
| 157 | +mawu { |
| 158 | + // Drop line numbers and local names from compiled scripts. Off by default: |
| 159 | + // a stack trace from a running script is worth more than the bytes. |
| 160 | + stripDebugInfo.set(false) |
| 161 | + |
| 162 | + // Add mawu-runtime to the implementation classpath. On by default. |
| 163 | + addRuntimeDependency.set(true) |
| 164 | + |
| 165 | + // Version of mawu-runtime to depend on. Defaults to the plugin's own. |
| 166 | + runtimeVersion.set("26.1") |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +## What Ends Up in the Jar |
| 171 | + |
| 172 | +Compiled scripts travel as resources, addressed by the path they had in the |
| 173 | +source set, without the extension: |
| 174 | + |
| 175 | +```text |
| 176 | +META-INF/mawu/scripts/hello.luac |
| 177 | +META-INF/mawu/scripts/lanes/worker.luac |
| 178 | +META-INF/mawu/scripts.index |
| 179 | +``` |
| 180 | + |
| 181 | +`src/main/lua/lanes/worker.lua` is therefore the script id `lanes/worker`. The |
| 182 | +index lists every id a jar carries, so an application can enumerate what it was |
| 183 | +built with. Nothing here is a `.class` file, and nothing is compiled again at |
| 184 | +startup. |
| 185 | + |
71 | 186 | ## Running Compiled Scripts |
72 | 187 |
|
73 | | -Compiled scripts ship as resources in your jar. At runtime, build a Luak environment, load a prototype and call it: |
| 188 | +`MawuScripts` reads the prototypes out of the classpath: |
| 189 | + |
| 190 | +```kotlin |
| 191 | +import net.blueva.luak.lib.jvm.JvmPlatform |
| 192 | +import net.blueva.mawu.runtime.MawuScripts |
| 193 | + |
| 194 | +val globals = JvmPlatform.standardGlobals() |
| 195 | +MawuScripts.run(globals, "hello") |
| 196 | +``` |
| 197 | + |
| 198 | +`load(id)` returns the prototype, `bind(globals, id)` returns it as a callable |
| 199 | +function, and `ids()` lists everything on the classpath. Bind one prototype into |
| 200 | +as many environments as you need: each is independent, so one script cannot |
| 201 | +reach into another's state. |
| 202 | + |
| 203 | +Prototypes are undumped directly rather than passed through `Globals.load`, so |
| 204 | +they still load in an environment configured to refuse binary chunks. |
| 205 | + |
| 206 | +## Calling Between Java and Lua |
| 207 | + |
| 208 | +A `MawuLane` is one Lua environment plus the calls that cross it in both |
| 209 | +directions. Lanes are independent: what one exposes, another never sees. |
74 | 210 |
|
75 | 211 | ```kotlin |
76 | | -import net.blueva.luak.lib.LuaPlatform |
| 212 | +import net.blueva.mawu.bridge.MawuLane |
| 213 | + |
| 214 | +val lane = MawuLane() |
| 215 | +``` |
| 216 | + |
| 217 | +### Java calling Lua |
| 218 | + |
| 219 | +```kotlin |
| 220 | +// A module script, one that ends in: return { greet = function(name) ... end } |
| 221 | +val api = lane.run("api") |
| 222 | + |
| 223 | +lane.callFunction(api.get("greet"), "world") // "hello world" |
| 224 | +lane.call("greet", "world") // a global function |
| 225 | +lane.call("net.request", 21) // one nested in a table |
| 226 | +lane.callMethod(api, "add", 2) // colon style: function M:add(value) |
| 227 | +``` |
| 228 | + |
| 229 | +Or with types, by handing the table to a Java interface: |
| 230 | + |
| 231 | +```java |
| 232 | +public interface Greeter { |
| 233 | + String greet(String name); |
| 234 | + default String shout(String name) { return greet(name).toUpperCase(); } |
| 235 | +} |
77 | 236 |
|
78 | | -val globals = LuaPlatform.standardGlobals() |
79 | | -globals.bind(prototype).call() |
| 237 | +Greeter greeter = lane.module("api", Greeter.class); |
| 238 | +greeter.greet("world"); |
80 | 239 | ``` |
81 | 240 |
|
82 | | -Each environment is independent, so one script cannot reach into another's state. |
| 241 | +Calls go to the table by name, so the script writes `function M.greet(name)` |
| 242 | +rather than `function M:greet(name)`. A method the table has no function for |
| 243 | +falls back to the interface's own default, and fails by name if there is none. |
83 | 244 |
|
84 | | -## Sandboxing |
| 245 | +### Lua calling Java |
85 | 246 |
|
86 | | -A host that runs Lua it did not write can set bounds on any `Globals` before binding a prototype to it: an instruction budget, a memory ceiling, a refusal to load binary chunks, a fixed random seed. These are Luak features and apply to Mawu-compiled scripts like any other. See the [Luak documentation](https://github.com/BluevaDevelopment/Luak) for the full list. |
| 247 | +A script names the classes it wants. Nothing has to be registered from the host |
| 248 | +first: |
| 249 | + |
| 250 | +```lua |
| 251 | +local Logger = java.util.logging.Logger |
| 252 | +local log = Logger:getLogger('my.plugin') |
| 253 | +log:info('hello') |
| 254 | + |
| 255 | +local Thing = import 'net.blueva.example.Thing' |
| 256 | +local text = java.lang.StringBuilder.new('a'):append('b'):toString() |
| 257 | +local level = java.util.logging.Level.WARNING |
| 258 | +``` |
| 259 | + |
| 260 | +A dependency of the build is on the classpath like anything else, so a script |
| 261 | +reaches a third-party library by name too: |
| 262 | + |
| 263 | +```kotlin |
| 264 | +dependencies { |
| 265 | + implementation("org.apache.commons:commons-lang3:3.19.0") |
| 266 | +} |
| 267 | +``` |
| 268 | + |
| 269 | +```lua |
| 270 | +local StringUtils = import 'org.apache.commons.lang3.StringUtils' |
| 271 | +result = StringUtils:reverse('mawu') |
| 272 | +``` |
| 273 | + |
| 274 | +Statics and fields come off the class (`Class:method(...)`, `Class.FIELD`), |
| 275 | +constructors are `Class.new(...)`, instance methods are `object:method(...)`, |
| 276 | +and a nested class is written with a dot, as `java.util.Map.Entry`. Overloaded |
| 277 | +methods pick the overload that fits the arguments. |
| 278 | + |
| 279 | +What the host hands over is live values, not permission: |
| 280 | + |
| 281 | +```kotlin |
| 282 | +lane.expose("calculator", Calculator()) // an object that already exists |
| 283 | +lane.exposeClass("Logger", Logger::class.java) // a short name for a long one |
| 284 | +lane.exposeFunction("log") { args -> println(args.first()); null } |
| 285 | +lane.exposeFunctions("host", mapOf("sum" to MawuFunction { args -> args.sumOf { it as Int } })) |
| 286 | +``` |
| 287 | + |
| 288 | +```lua |
| 289 | +local total = calculator:add(40, 2) |
| 290 | +log('total is ' .. total) |
| 291 | +local sum = host.sum(1, 2, 3) |
| 292 | +``` |
| 293 | + |
| 294 | +### What crosses |
| 295 | + |
| 296 | +| Lua | Java | |
| 297 | +|---|---| |
| 298 | +| `nil` | `null` | |
| 299 | +| boolean | `Boolean` | |
| 300 | +| integer | `Integer` | |
| 301 | +| float | `Double` | |
| 302 | +| string | `String` | |
| 303 | +| a Java object the host exposed | itself, methods and all | |
| 304 | +| table, function | `LuaValue`, or a Java interface through `module`/`proxy` | |
| 305 | + |
| 306 | +### What a script can reach |
| 307 | + |
| 308 | +A lane's environment is `JvmPlatform.standardGlobals()` by default, plus `java` |
| 309 | +and `import`. The scripts are your project's own source, as trusted as the |
| 310 | +Kotlin next to them, so they reach the classpath the same way it does. |
| 311 | + |
| 312 | +Two ways out of that, for a host that runs Lua it did not write: |
| 313 | +`MawuLane(exposeClasspath = false)` drops `java` and `import`, and passing a |
| 314 | +`Globals` of your own decides the rest. Luak has the bounds for that case (an |
| 315 | +instruction budget, a memory ceiling, no binary chunks), and they work on |
| 316 | +Mawu-compiled prototypes like on any other. |
87 | 317 |
|
88 | 318 | ## Authors |
89 | 319 |
|
|
0 commit comments