Skip to content

Commit 2b00178

Browse files
committed
Add push(enum:) API
1 parent de53268 commit 2b00178

5 files changed

Lines changed: 210 additions & 4 deletions

File tree

‎Sources/Lua/Documentation.docc/Articles/BridgingSwiftToLua.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Basic Swift types are pushed by value -- that is to say they are copied and conv
1010

1111
Bridged values on the other hand are represented in Lua using the `userdata` Lua type, which from the Swift side behave as if there was an assignment like `var userdataVar: T? = myval` (where `T` is the type used in the `Metatable`, described below). So for classes, the `userdata` holds an additional reference to the object, and for structs the `userdata` holds a value copy of it. A `__gc` metamethod is automatically generated, which means that when the `userdata` is garbage collected by Lua, the equivalent of `userdataVar = nil` is performed.
1212

13-
> Note: While defining metatables for `struct` types is supported, all userdata in Lua are copy-by-reference, so the object will behave more like a class from the Lua side. Overall, `class` types can be a better fit for how the bridging logic behaves.
13+
> Note: While defining metatables for `struct` (and `enum`) types is supported, all userdata in Lua are copy-by-reference, so the object will behave more like a class from the Lua side. Overall, `class` types can be a better fit for how the bridging logic behaves.
1414
1515
As described so far, the `userdata` plays nicely with Lua and Swift object lifetimes and memory management, but does not allow you to do anything useful with it from Lua other than controlling when it goes out of scope. This is where defining a metatable comes in.
1616

@@ -73,9 +73,11 @@ L.register(Metatable<Foo>(fields: [
7373

7474
Any arguments to the closure are type-checked using using `L.checkArgument<ArgumentType>()`. Anything which exceeds the type inference abilities of `memberfn` can always be written explicitly using `closure`. The full list of helpers that can be used to define fields is defined in ``Metatable/FieldType``. Note there are multiple overloads of `memberfn` to accommodate different numbers of arguments.
7575

76+
> Note: The above description skipped some of the nuances that, for example, avoid copying the value when using `.closure` with a struct type, which would entail using `checkUserdata(1)` rather than `checkArgument(1)`. Use `.memberfn` where possible which hides that complexity.
77+
7678
## Pushing values into Lua
7779

78-
Having defined a metatable for our type, we can use [`push(userdata:)`](doc:Lua/Swift/UnsafeMutablePointer/push(userdata:toindex:)) or [`push(any:)`](doc:Lua/Swift/UnsafeMutablePointer/push(any:toindex:)) to push instances of it on to the Lua stack, at which point we can assign it to a variable just like any other Lua value. Using the example `Foo` class described above, and assuming our Lua code expects a single global value called `foo` to be defined, we could use ``Lua/Swift/UnsafeMutablePointer/setglobal(name:)``:
80+
Having defined a metatable for our type by calling `register()`, we can use [`push(userdata:)`](doc:Lua/Swift/UnsafeMutablePointer/push(userdata:toindex:)) or [`push(any:)`](doc:Lua/Swift/UnsafeMutablePointer/push(any:toindex:)) to push instances of it on to the Lua stack, at which point we can assign it to a variable just like any other Lua value. Using the example `Foo` class described above, and assuming our Lua code expects a single global value called `foo` to be defined, we could use ``Lua/Swift/UnsafeMutablePointer/setglobal(name:)``:
7981

8082
```swift
8183
let foo = Foo()
@@ -155,7 +157,7 @@ To customize the bridging above and beyond adding fields to the userdata, we can
155157
```swift
156158
L.register(Metatable<Foo>(
157159
call: .memberfn { obj in
158-
print("I have no idea what this should do")
160+
print("object was called")
159161
},
160162
close: .memberfn { obj in
161163
// Do whatever is appropriate to obj here,

‎Sources/Lua/Documentation.docc/Articles/LuaState.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ Some Lua C APIs do not make sense to be called from Swift; usually this is becau
288288
- ``Lua/Swift/UnsafeMutablePointer/push(tuple:)``
289289
- ``Lua/Swift/UnsafeMutablePointer/pushthread()``
290290
- ``Lua/Swift/UnsafeMutablePointer/pushglobals(toindex:)-3ot28``
291+
- ``Lua/Swift/UnsafeMutablePointer/push(enum:toindex:)``
291292

292293
### Iterators
293294

‎Sources/Lua/LuaState.swift‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2855,6 +2855,43 @@ extension UnsafeMutablePointer where Pointee == lua_State {
28552855
}
28562856
}
28572857

2858+
/// Pushes a table of all the cases of an enum on to the stack.
2859+
///
2860+
/// Push all the cases of an enum on to the stack, as a table mapping case names to values. This is useful to make
2861+
/// an enum available for use by Lua code. The enum must be `CaseIterable` and ``Pushable`` -- this is most easily
2862+
/// done by also inheriting ``RawPushable``.
2863+
///
2864+
/// For example:
2865+
/// ```swift
2866+
/// enum MyEnum: Int, CaseIterable, RawPushable {
2867+
/// case foo = 1
2868+
/// case bar = 2
2869+
/// }
2870+
///
2871+
/// L.push(enum: MyEnum.self)
2872+
/// L.setglobal(name: "MyEnum")
2873+
/// try L.dostring("print(MyEnum.bar)") // prints "2"
2874+
/// ```
2875+
///
2876+
/// The above `push` and `setglobal` calls could also be combined by using the [`.enum`](doc:Pushable/enum(_:))
2877+
/// `Pushable` helper.
2878+
///
2879+
/// > Note: This function is for exposing the entire declaration of an enum to Lua. To push a particular enum value,
2880+
/// treat it like any other pushable value and call [`push(value)`](doc:push(_:toindex:)-59fx9).
2881+
///
2882+
/// - Parameter enumType: The enum to push on to the Lua stack, which must inherit `CaseIterable` and `Pushable`.
2883+
/// - Parameter toindex: See <doc:LuaState#Push-functions-toindex-parameter>.
2884+
public func push<T>(enum enumType: T.Type, toindex: CInt = -1) where T: CaseIterable & Pushable {
2885+
let cases = enumType.allCases
2886+
newtable(nrec: CInt(exactly: cases.count) ?? 0)
2887+
for c in cases {
2888+
rawset(-1, utf8Key: "\(c)", value: c)
2889+
}
2890+
if toindex != -1 {
2891+
insert(toindex)
2892+
}
2893+
}
2894+
28582895
// MARK: - Calling into Lua
28592896

28602897
/// Make a protected call to a Lua function, optionally including a stack trace in any errors.

‎Sources/Lua/Pushable.swift‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ public struct LuaUserdataWrapper: Pushable {
148148
}
149149
}
150150

151+
/// A `Pushable` wrapper that pushes an enum.
152+
///
153+
/// See ``Pushable/enum(_:)``.
154+
public struct LuaEnumWrapper<T>: Pushable where T: CaseIterable & Pushable {
155+
public func push(onto L: LuaState) {
156+
L.push(enum: T.self)
157+
}
158+
}
159+
151160
public struct _NonPushableTypesHelper: Pushable {
152161
private init() {}
153162
public func push(onto L: LuaState) {
@@ -227,4 +236,45 @@ extension Pushable where Self == _NonPushableTypesHelper {
227236
public static func userdata(_ val: Any) -> LuaUserdataWrapper {
228237
return LuaUserdataWrapper(value: val)
229238
}
239+
240+
/// Returns a Pushable which pushes its value using `push(enum:)`.
241+
///
242+
/// This permits the use of `.enum(enumType)` anywhere a Pushable can be specified. For example to define a global
243+
/// value that exposes all the values of an enum `Foo`:
244+
///
245+
/// ```swift
246+
/// enum Foo: String, CaseIterable {
247+
/// case someThing
248+
/// case otherThing
249+
/// }
250+
///
251+
/// L.setglobal(name: "Foo", value: .enum(Foo.self))
252+
/// // You can now do `Foo.someThing` etc from Lua.
253+
/// ```
254+
public static func `enum`<T>(_ e: T.Type) -> LuaEnumWrapper<T> where T: CaseIterable & Pushable {
255+
return LuaEnumWrapper<T>()
256+
}
257+
}
258+
259+
/// Protocol for making a `RawRepresentable` type be `Pushable` using its `rawValue`.
260+
///
261+
/// By declaring that a type conforms to this protocol, the type becomes `Pushable` using its `rawValue`. For example,
262+
/// you can use this to make an enum `Pushable`:
263+
///
264+
/// ```swift
265+
/// enum E: String, RawPushable {
266+
/// case one
267+
/// case two
268+
/// }
269+
///
270+
/// L.push(E.one) // pushes the string "one" onto the stack
271+
/// ```
272+
///
273+
/// You do not need to supply an implementation of `Pushable.push(onto:)` -- one is created automatically.
274+
public protocol RawPushable: Pushable, RawRepresentable {}
275+
276+
public extension RawPushable {
277+
func push(onto state: LuaState) {
278+
state.push(any: self.rawValue)
279+
}
230280
}

‎Tests/lua-test/LuaTests.swift‎

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,7 @@ final class LuaTests: XCTestCase {
15281528
"foo": .memberfn { $0.foo() }
15291529
])}
15301530
}
1531-
L.register(DefaultMetatable()) // Make sure pushMetatable doesn't create a metatable for it
1531+
L.register(DefaultMetatable()) // This would prevent pushMetatable from creating an empty metatable for it
15321532
XCTAssertFalse(L.isMetatableRegistered(for: Foo.self))
15331533
L.pushMetatable(for: Foo.self)
15341534
XCTAssertTrue(L.isMetatableRegistered(for: Foo.self)) // Won't return true if the default ended up being used
@@ -4251,6 +4251,122 @@ final class LuaTests: XCTestCase {
42514251
XCTAssertTrue(name.hasPrefix("ItsFoo: "), "Wrong name \(name)")
42524252
}
42534253

4254+
func test_push_enum() throws {
4255+
enum EStr: String, CaseIterable, RawPushable {
4256+
case one
4257+
case two
4258+
case three
4259+
}
4260+
L.push(enum: EStr.self)
4261+
L.setglobal(name: "EStr")
4262+
let estrdict: [String: String]? = L.globals["EStr"].tovalue()
4263+
XCTAssertEqual(estrdict, ["one": "one", "two": "two", "three": "three"])
4264+
4265+
enum EInt: Int, CaseIterable, RawPushable {
4266+
case one = 1
4267+
case two = 22
4268+
case three = 333
4269+
}
4270+
L.setglobal(name: "EInt", value: .enum(EInt.self))
4271+
let eintdict: [String: Int]? = L.globals["EInt"].tovalue()
4272+
XCTAssertEqual(eintdict, ["one": 1, "two": 22, "three": 333])
4273+
4274+
4275+
enum ERawPushable: String, CaseIterable, Pushable {
4276+
case one
4277+
case two
4278+
case three
4279+
4280+
func push(onto state: LuaState) {
4281+
state.push(userdata: self)
4282+
}
4283+
}
4284+
4285+
L.push(enum: ERawPushable.self)
4286+
L.setglobal(name: "ERawPushable")
4287+
// We're only checking the above compiles, so no need to do anything with it.
4288+
}
4289+
4290+
func test_RawPushable_enum() throws {
4291+
enum E: String, RawPushable {
4292+
case one
4293+
case two
4294+
}
42544295

4296+
L.push(E.one)
4297+
XCTAssertEqual(L.tovalue(1), "one")
4298+
}
4299+
4300+
func test_associated_value_enum() throws {
4301+
enum PushableEnum: Equatable, PushableWithMetatable {
4302+
case foo
4303+
case bar(Int)
4304+
case baz(Int, String)
4305+
4306+
static var metatable: Metatable<PushableEnum> {
4307+
return Metatable<PushableEnum>(fields: [
4308+
"type": .property {
4309+
switch $0 {
4310+
case .foo: return "foo"
4311+
case .bar(_): return "bar"
4312+
case .baz(_, _): return "baz"
4313+
}
4314+
},
4315+
"barval": .property(get: { val -> Int? in
4316+
if case .bar(let barval) = val {
4317+
return barval
4318+
} else {
4319+
return nil
4320+
}
4321+
}),
4322+
"bazint": .property {
4323+
let ret: Int?
4324+
if case .baz(let bazint, _) = $0 {
4325+
ret = bazint
4326+
} else {
4327+
ret = nil
4328+
}
4329+
return ret
4330+
},
4331+
"bazstr": .property {
4332+
let ret: String?
4333+
if case .baz(_, let bazstr) = $0 {
4334+
ret = bazstr
4335+
} else {
4336+
ret = nil
4337+
}
4338+
return ret
4339+
}
4340+
],
4341+
eq: .synthesize)
4342+
}
4343+
}
4344+
4345+
L.push(PushableEnum.foo)
4346+
L.push(PushableEnum.bar(42))
4347+
L.push(PushableEnum.baz(43, "hello"))
4348+
L.push(PushableEnum.baz(43, "hello"))
4349+
L.push(PushableEnum.baz(43, "nope"))
4350+
XCTAssertTrue(try L.equal(1, 1)) // Will be true regardless of eq because they are the same object
4351+
XCTAssertFalse(try L.equal(1, 2))
4352+
XCTAssertTrue(try L.equal(3, 4)) // Requires the eq metamethod
4353+
XCTAssertFalse(try L.equal(3, 5))
4354+
4355+
XCTAssertEqual(L.tovalue(1), PushableEnum.foo)
4356+
XCTAssertEqual(L.tovalue(2), PushableEnum.bar(42))
4357+
XCTAssertEqual(L.tovalue(3), PushableEnum.baz(43, "hello"))
4358+
4359+
L.pop(2) // baz2 and baz3
4360+
L.setglobal(name: "baz")
4361+
L.setglobal(name: "bar")
4362+
L.setglobal(name: "foo")
4363+
4364+
try L.dostring("assert(foo.type == 'foo')")
4365+
try L.dostring("assert(bar.type == 'bar')")
4366+
try L.dostring("assert(baz.type == 'baz')")
4367+
4368+
XCTAssertNil(L.globals["foo"]["barval"].toint())
4369+
XCTAssertEqual(L.globals["bar"]["barval"].toint(), 42)
4370+
}
42554371
}
42564372

0 commit comments

Comments
 (0)