diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8829d74f..0ecbebd3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -448,6 +448,7 @@ jobs: cargo playdate package --features=$FEATURES --examples --device --simulator -p=playdate-sound cargo playdate package --features=$FEATURES --examples --device --simulator -p=playdate-sprite cargo playdate package --features=$FEATURES --examples --device --simulator -p=playdate-system + cargo playdate package --features=$FEATURES --examples --device --simulator -p=playdate-lua cargo playdate package --features=lang-items,entry-point --examples --device --simulator -p=playdate # TODO: build crankstart with examples for compatibility test diff --git a/Cargo.lock b/Cargo.lock index ab62499a..cf5a5803 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4050,12 +4050,13 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "playdate" -version = "0.1.17" +version = "0.2.0" dependencies = [ "playdate-controls", "playdate-display", "playdate-fs", "playdate-graphics", + "playdate-lua", "playdate-menu", "playdate-scoreboards", "playdate-sound", @@ -4190,6 +4191,14 @@ dependencies = [ "playdate-system", ] +[[package]] +name = "playdate-lua" +version = "0.1.0" +dependencies = [ + "playdate-sys", + "playdate-system", +] + [[package]] name = "playdate-menu" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index b3c59586..a8a6747e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ ctrl = { version = "0.3", path = "api/ctrl", package = "playdate-controls", defa display = { version = "0.3", path = "api/display", package = "playdate-display", default-features = false } fs = { version = "0.2", path = "api/fs", package = "playdate-fs", default-features = false } gfx = { version = "0.3", path = "api/gfx", package = "playdate-graphics", default-features = false } +lua = { version = "0.1", path = "api/lua", package = "playdate-lua", default-features = false } menu = { version = "0.2", path = "api/menu", package = "playdate-menu", default-features = false } scoreboards = { version = "0.1", path = "api/scoreboards", package = "playdate-scoreboards", default-features = false } sound = { version = "0.3", path = "api/sound", package = "playdate-sound", default-features = false } diff --git a/api/lua/Cargo.toml b/api/lua/Cargo.toml new file mode 100644 index 00000000..706d0214 --- /dev/null +++ b/api/lua/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "playdate-lua" +version = "0.1.0" +readme = "README.md" +description = "High-level Lua API built on-top of Playdate API" +keywords = ["playdate", "sdk", "api", "gamedev"] +categories = ["game-development", "api-bindings", "no-std"] +edition.workspace = true +license.workspace = true +authors = ["Paul Young", "Alex Koz "] +homepage.workspace = true +repository.workspace = true + + +[features] +default = ["sys/default"] + +# playdate-sys features, should be shared because it's build configuration: + +bindgen-runtime = ["sys/bindgen-runtime"] +bindgen-static = ["sys/bindgen-static"] +bindings-derive-debug = ["sys/bindings-derive-debug"] + + +[dependencies] +sys = { workspace = true, default-features = false } + +[dev-dependencies] +system = { workspace = true, default-features = false } + + +[[example]] +name = "add-function-get-arg-string" +crate-type = ["dylib", "staticlib"] +path = "examples/add-function-get-arg-string.rs" +required-features = ["sys/entry-point", "sys/lang-items"] + + +[package.metadata.playdate] +bundle-id = "rs.playdate.lua" + +[package.metadata.playdate.dev-assets] +# The Lua runtime expects us to provide a main.pdz file at the root. +# +# Compiled with `pdc --skip-unknown --strip sources/main.lua Example.pdx` +"main.pdz" = "examples/Example.pdx/main.pdz" + + +[package.metadata.docs.rs] +all-features = false +features = [ + "sys/bindings-derive-default", + "sys/bindings-derive-eq", + "sys/bindings-derive-copy", + "bindings-derive-debug", + "sys/bindings-derive-hash", + "sys/bindings-derive-ord", + "sys/bindings-derive-partialeq", + "sys/bindings-derive-partialord", +] +rustdoc-args = ["--cfg", "docsrs", "--show-type-layout"] +default-target = "thumbv7em-none-eabihf" +cargo-args = [ + "-Zunstable-options", + "-Zrustdoc-scrape-examples", + "-Zbuild-std=core,alloc", +] diff --git a/api/lua/README.md b/api/lua/README.md new file mode 100644 index 00000000..d0490b06 --- /dev/null +++ b/api/lua/README.md @@ -0,0 +1,17 @@ +# Lua API for PlayDate + +High-level Lua API built on-top of [playdate-sys][]. + + +## Usage + +See [examples][]. + +[examples]: ./examples +[playdate-sys]: https://crates.io/crates/playdate-sys + + + +- - - + +This software is not sponsored or supported by Panic. diff --git a/api/lua/examples/Example.pdx/main.pdz b/api/lua/examples/Example.pdx/main.pdz new file mode 100644 index 00000000..4f7e1a8b Binary files /dev/null and b/api/lua/examples/Example.pdx/main.pdz differ diff --git a/api/lua/examples/Example.pdx/pdxinfo b/api/lua/examples/Example.pdx/pdxinfo new file mode 100644 index 00000000..5614746b --- /dev/null +++ b/api/lua/examples/Example.pdx/pdxinfo @@ -0,0 +1,2 @@ +pdxversion=20400 +buildtime=767932885 diff --git a/api/lua/examples/README.md b/api/lua/examples/README.md new file mode 100644 index 00000000..fdc577ba --- /dev/null +++ b/api/lua/examples/README.md @@ -0,0 +1,16 @@ +# Examples + +These examples additionally use other crates with parts of Playdate API to minimize the amount of code. + + +# How to run + +```bash +cargo playdate run -p=playdate-lua --example=add-function-get-arg-string --features=sys/lang-items,sys/entry-point +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/lua/examples/add-function-get-arg-string.rs b/api/lua/examples/add-function-get-arg-string.rs new file mode 100644 index 00000000..0017bf3e --- /dev/null +++ b/api/lua/examples/add-function-get-arg-string.rs @@ -0,0 +1,68 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate_lua as lua; + +use core::ffi::c_int; +use core::ptr::NonNull; + +use lua::Lua; +use sys::EventLoopCtrl; +use sys::ffi::*; +use system::System; +use system::event::SystemEventExt as _; +use system::update::UpdateCtrl; + + +/// Entry point, event handler +#[no_mangle] +fn event_handler(_api: NonNull, event: PDSystemEvent, _: u32) -> EventLoopCtrl { + // We need to set our update callback in the InitLua handler instead of Init. + // https://devforum.play.date/t/lua-c-minimal-example/4354/5 + // + // Just for this example, ignore all other events. + if event != PDSystemEvent::InitLua { + return EventLoopCtrl::Continue; + } + + // Set update callback + System::Default().set_update_callback_static(Some(on_update), ()); + + // Add a function that we depend on and call in main.lua + Lua::Default().add_function(Some(log_to_console_from_main_dot_lua), "example.logToConsole") + .expect("add_function 'log_to_console_from_main_dot_lua' should succeed"); + + // Continue event loop + EventLoopCtrl::Continue +} + + +/// Update handler +fn on_update(_: &mut ()) -> UpdateCtrl { + // Continue updates + UpdateCtrl::Continue +} + + +// The function we add to the Lua runtime and call from main.lua +pub unsafe extern "C" fn log_to_console_from_main_dot_lua(_lua_state: *mut lua_State) -> c_int { + // We know that our function takes a single argument which is a string. + let arg_string = Lua::Default().get_arg_string(1) + .expect("get_arg_string should succeed"); + + // Avoid going from CString to str and back with playdate::sys::log::println + let f = (*(*sys::API).system).logToConsole + .expect("get logToConsole to succeed"); + + f(arg_string.as_ptr()); + + // A `lua_CFunction` should return the number of return values it has pushed + // onto the stack. + 0 +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/lua/examples/sources/main.lua b/api/lua/examples/sources/main.lua new file mode 100644 index 00000000..1dac3f02 --- /dev/null +++ b/api/lua/examples/sources/main.lua @@ -0,0 +1 @@ +example.logToConsole("hello from main.lua"); diff --git a/api/lua/src/error.rs b/api/lua/src/error.rs new file mode 100644 index 00000000..3dd7b9c3 --- /dev/null +++ b/api/lua/src/error.rs @@ -0,0 +1,38 @@ +use alloc::borrow::ToOwned; +use core::fmt; +use sys::ffi::CStr; +use sys::ffi::CString; + + +pub type ApiError = sys::error::Error; + + +#[derive(Debug)] +pub enum Error { + AddFunction(CString), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + Error::AddFunction(cs) => { + match cs.to_str() { + Ok(err) => err.fmt(f), + Err(_) => f.write_fmt(format_args!("Add function error: {cs:?}")), + } + }, + } + } +} + + +impl From for ApiError { + fn from(err: Error) -> Self { ApiError::Api(err) } +} + +impl From<&'_ CStr> for Error { + fn from(cs: &CStr) -> Self { Self::AddFunction(cs.to_owned()) } +} + + +impl core::error::Error for Error {} diff --git a/api/lua/src/lib.rs b/api/lua/src/lib.rs new file mode 100644 index 00000000..79fc4f3f --- /dev/null +++ b/api/lua/src/lib.rs @@ -0,0 +1,175 @@ +#![cfg_attr(not(test), no_std)] +#![feature(error_in_core)] + +// #[macro_use] +extern crate sys; +extern crate alloc; + +use core::ffi::c_char; +use alloc::borrow::ToOwned; + +use sys::ffi::CStr; +use sys::ffi::CString; +use sys::ffi::lua_CFunction; + + +pub mod error; + +use error::*; + +#[derive(Debug, Clone, Copy)] +pub struct Lua(Api); + +impl Lua { + /// Creates default [`Lua`] without type parameter requirement. + /// + /// Uses ZST [`api::Default`]. + #[allow(non_snake_case)] + pub fn Default() -> Self { Self(Default::default()) } +} + +impl Lua { + /// Creates [`Lua`] without type parameter requirement. + /// + /// Uses [`api::Cache`]. + #[allow(non_snake_case)] + pub fn Cached() -> Self { Self(Default::default()) } +} + +impl Default for Lua { + fn default() -> Self { Self(Default::default()) } +} + +impl Lua { + pub fn new() -> Self { Self(Default::default()) } +} + +impl Lua { + pub fn new_with(api: Api) -> Self { Self(api) } +} + + +impl Lua {} + + +impl Lua { + /// Adds the Lua function *f* to the Lua runtime, with name *name*. (*name* + /// can be a table path using dots, e.g. if name = “mycode.myDrawingFunction” + /// adds the function “myDrawingFunction” to the global table “myCode”.) + /// + /// Equivalent to [`sys::ffi::playdate_lua::addFunction`] + #[doc(alias = "sys::ffi::playdate_lua::addFunction")] + pub fn add_function>(&self, f: lua_CFunction, name: S) -> Result<(), ApiError> { + let name = CString::new(name.as_ref())?; + let mut out_err: *const c_char = core::ptr::null_mut(); + + let func = self.0.add_function(); + + // Returns 1 on success or 0 with an error message in *outErr*. + let result = unsafe { func(f, name.as_ptr(), &mut out_err) }; + + if result == 0 { + let err_msg = unsafe { CStr::from_ptr(out_err) }; + Err(Error::AddFunction(err_msg.to_owned()).into()) + } else { + Ok(()) + } + } + + /// Returns the argument at position *pos* as a string. + /// + /// Equivalent to [`sys::ffi::playdate_lua::getArgString`] + #[doc(alias = "sys::ffi::playdate_lua::getArgString")] + pub fn get_arg_string(&self, pos: i32) -> Option { + let f = self.0.get_arg_string(); + unsafe { + let ptr = f(pos); + if ptr.is_null() { + None + } else { + Some(CStr::from_ptr(ptr).to_owned()) + } + } + } +} + +pub mod api { + use core::ffi::c_char; + use core::ffi::c_int; + use core::ptr::NonNull; + use sys::ffi::lua_CFunction; + use sys::ffi::playdate_lua; + + + /// Default lua api end-point, ZST. + /// + /// All calls approximately costs ~3 derefs. + #[derive(Debug, Clone, Copy, core::default::Default)] + pub struct Default; + impl Api for Default {} + + + /// Cached lua api end-point. + /// + /// Stores one reference, so size on stack is eq `usize`. + /// + /// All calls approximately costs ~1 deref. + #[derive(Clone, Copy)] + #[cfg_attr(feature = "bindings-derive-debug", derive(Debug))] + pub struct Cache(&'static playdate_lua); + + impl core::default::Default for Cache { + fn default() -> Self { Self(sys::api!(lua)) } + } + + impl From<*const playdate_lua> for Cache { + #[inline(always)] + fn from(ptr: *const playdate_lua) -> Self { Self(unsafe { ptr.as_ref() }.expect("lua")) } + } + + impl From<&'static playdate_lua> for Cache { + #[inline(always)] + fn from(r: &'static playdate_lua) -> Self { Self(r) } + } + + impl From> for Cache { + #[inline(always)] + fn from(ptr: NonNull) -> Self { Self(unsafe { ptr.as_ref() }) } + } + + impl From<&'_ NonNull> for Cache { + #[inline(always)] + fn from(ptr: &NonNull) -> Self { Self(unsafe { ptr.as_ref() }) } + } + + + impl Api for Cache { + #[inline(always)] + fn add_function( + &self) + -> unsafe extern "C" fn(f: lua_CFunction, name: *const c_char, outErr: *mut *const c_char) -> c_int { + self.0.addFunction.expect("addFunction") + } + + #[inline(always)] + fn get_arg_string(&self) -> unsafe extern "C" fn(pos: c_int) -> *const c_char { + self.0.getArgString.expect("getArgString") + } + } + + + pub trait Api { + /// Returns [`sys::ffi::playdate_lua::addFunction`] + #[doc(alias = "sys::ffi::playdate_lua::addFunction")] + fn add_function( + &self) + -> unsafe extern "C" fn(f: lua_CFunction, name: *const c_char, outErr: *mut *const c_char) -> c_int { + *sys::api!(lua.addFunction) + } + /// Returns [`sys::ffi::playdate_lua::getArgString`] + #[doc(alias = "sys::ffi::playdate_lua::getArgString")] + fn get_arg_string(&self) -> unsafe extern "C" fn(pos: c_int) -> *const c_char { + *sys::api!(lua.getArgString) + } + } +} diff --git a/api/playdate/Cargo.toml b/api/playdate/Cargo.toml index bc28d5dd..25ccc8ac 100644 --- a/api/playdate/Cargo.toml +++ b/api/playdate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate" -version = "0.1.17" +version = "0.2.0" readme = "README.md" description = "High-level Playdate API" keywords = ["playdate", "sdk", "api", "gamedev"] @@ -17,6 +17,7 @@ ctrl = { workspace = true, default-features = false } display = { workspace = true, default-features = false } fs = { workspace = true, default-features = false } gfx = { workspace = true, default-features = false } +lua = { workspace = true, default-features = false } menu = { workspace = true, default-features = false } scoreboards = { workspace = true, default-features = false } sound = { workspace = true, default-features = false } diff --git a/api/playdate/README.md b/api/playdate/README.md index 5b28dee9..6f5d980e 100644 --- a/api/playdate/README.md +++ b/api/playdate/README.md @@ -16,6 +16,7 @@ Usage with [cargo-playdate][cargo-playdate] is strongly recommended. - [display](https://crates.io/crates/playdate-display) - [file system](https://crates.io/crates/playdate-fs) - [graphics](https://crates.io/crates/playdate-graphics) (with [color](https://crates.io/crates/playdate-color)) +- [lua](https://crates.io/crates/playdate-lua) - [scoreboards](https://crates.io/crates/playdate-scoreboards) - [sound](https://crates.io/crates/playdate-sound) - [sprite](https://crates.io/crates/playdate-sprite) @@ -28,7 +29,6 @@ Plus some extensions to make it all more rust-ish. ### Not yet covered parts: - json -- lua ## How to start diff --git a/api/playdate/src/lib.rs b/api/playdate/src/lib.rs index 3b4d0718..14aa8298 100644 --- a/api/playdate/src/lib.rs +++ b/api/playdate/src/lib.rs @@ -34,6 +34,10 @@ pub mod fs { pub use fs::prelude::*; } +pub mod lua { + pub use lua::*; +} + pub mod ext { use core::ptr::NonNull; @@ -61,7 +65,9 @@ pub mod ext { /// Playdate Sound API. fn sound(&self) -> sound::Sound; - // fn lua() -> lua::Lua; + /// Playdate Lua API. + fn lua(&self) -> lua::Lua; + // fn json() -> json::Json; fn scoreboards(&self) -> scoreboards::Scoreboards; @@ -97,6 +103,10 @@ pub mod ext { fn scoreboards(&self) -> scoreboards::Scoreboards { scoreboards::Scoreboards::new_with(scoreboards::api::Cache::from(unsafe { self.as_ref() }.scoreboards)) } + + fn lua(&self) -> lua::Lua { + lua::Lua::new_with(lua::api::Cache::from(unsafe { self.as_ref() }.lua)) + } } impl PlaydateAPIExt for *const sys::ffi::PlaydateAPI { @@ -129,5 +139,9 @@ pub mod ext { let api = scoreboards::api::Cache::from(unsafe { self.as_ref() }.expect("api").scoreboards); scoreboards::Scoreboards::new_with(api) } + + fn lua(&self) -> lua::Lua { + lua::Lua::new_with(lua::api::Cache::from(unsafe { self.as_ref() }.expect("api").lua)) + } } }