Sweet Moon
Sweet Moon is a resilient solution that makes working with Lua / Fennel from Ruby and vice versa a delightful experience.
- Supported Versions
- Setup and TLDR
- Loading Configuration Files
- Lua Configuration Files
- Fennel Configuration Files
- Performance and Benchmarks
- Fennel and Lua Versions
- Comparison with other Gems
- Interacting with a Lua State
- Setup
- Exchanging Data
- eval and load
- Primitives
- Tables, Arrays, and Hashes
- Functions
- Other Types
- Lua Global vs Local Variables
- destroy and clear
- Modules, Packages and LuaRocks
- Integration with LuaRocks
- Fennel
- Fennel Usage
- Fennel Global vs Local Variables
- allowedGlobals and options
- Fennel Setup
- Integration with fnx
- Fennel REPL
- Global vs Isolated
- Global FFI
- Error Handling
- Ruby feat. Lua Errors
- Where can I find .so files?
- Low-Level C API
- The API
- Custom Shared Objects
- Custom API References
- Functions, Macros and Signatures
- Low-Level C API Example
- Lua 5.4
- Lua 4.0
- Development
- Tests Setup
- Running
- Publish to RubyGems
- Supporting New Versions
Supported Versions
Sweet Moon was created to be resilient and adaptable. So it doesn't have a dependency on specific versions, and it will always try to create a working environment with whatever you have available.
That said, these are the officially tested versions:
C API:
- Lua:
3.2.2
,4.0.1
,5.0.3
,5.1.4
, and5.4.2
Interpreter:
- Lua:
5.0
,5.1
, and5.4
Interpreters' Compatibility:
- Lua:
5.0.3
,5.1.4
,5.1.5
,5.2.4
,5.3.3
,5.4.2
, and5.4.4
- LuaJIT:
2.0.5
Setup and TLDR
gem install sweet-moon
Disclaimer: It's an early-stage project, and you should expect breaking changes.
gem 'sweet-moon', '~> 1.0.0'
require 'sweet-moon'
# State
SweetMoon.global.state.eval('return 1 + 2') # => 3
SweetMoon.global.state.fennel.eval('(+ 2 3)') # => 5
state = SweetMoon::State.new
state.eval('return 3 + 4') # => 7
state.load('file.lua') # => {...}
state.fennel.eval('(+ 3 7)') # => 10
state.fennel.load('file.fnl') # => {...}
# API
SweetMoon.global.api.luaL_newstate
api = SweetMoon::API.new
state = api.luaL_newstate
api.luaL_openlibs(state)
Loading Configuration Files
Lua as a Configuration Language is a robust approach widely used in the industry for decades. It's a powerful alternative to YAML or TOML and way more spread and battle-tested than edn.
Lua Configuration Files
Create a .lua
file:
return {
color = "red",
dimensions = { width = 200, height = 2 * 80 },
values = {4, 6} }
Load it:
require 'sweet-moon'
SweetMoon.global.state.load('config.lua')
# => { 'color' => 'red',
# 'dimensions' => { 'width' => 200, 'height' => 160 },
# 'values' => [4, 6] }
Alternatively:
require 'sweet-moon'
state = SweetMoon::State.new
state.load('config.lua')
Fennel Configuration Files
Create a .fnl
file:
{:color "red"
:dimensions {:width 200 :height (* 2 80)}
:values [4 6]}
Load it:
require 'sweet-moon'
SweetMoon.global.state.fennel.load('config.fnl')
# => { 'color' => 'red',
# 'dimensions' => { 'width' => 200, 'height' => 160 },
# 'values' => [4, 6] }
Alternatively:
require 'sweet-moon'
state = SweetMoon::State.new
state.fennel.load('config.fnl')
Performance and Benchmarks
Benchmarks created through benchmark-ips.
The task is to get a file with a source code equivalent to:
return {
color = "red",
dimensions = { width = 200, height = 2 * 80 },
values = {4, 6} }
And then bring the final Ruby representation:
{ 'color' => 'red',
'dimensions' => { 'width' => 200, 'height' => 160 },
'values' => [4, 6] }
It is important to note that only Lua and Fennel natively support expressions like 2 * 80
, and the other solutions have only a static number in their source.
Fennel and Lua Versions
Higher is better:
Comparison with other Gems
Higher is better:
Compared to: rufus-lua, YAML, edn-ruby, toml-rb, and toml.
Interacting with a Lua State
Lua is a fast language engine with small footprint that you can embed easily into your application.
- Setup
- Exchanging Data
- eval and load
- Primitives
- Tables, Arrays, and Hashes
- Functions
- Other Types
- Lua Global vs Local Variables
- destroy and clear
Setup
A state is composed of three key elements: shared_object
, api_reference
, and interpreter
.
For the global state:
require 'sweet-moon'
SweetMoon.global.config(
shared_object: '/usr/lib/liblua.so.5.4.4',
api_reference: '5.4.2',
interpreter: '5.4'
)
SweetMoon.global.state.eval('return 1 + 1') # => 2
For a new isolated state:
require 'sweet-moon'
state = SweetMoon::State.new(
shared_object: '/usr/lib/liblua.so.5.4.4',
api_reference: '5.4.2',
interpreter: '5.4'
)
state.eval('return 1 + 1') # => 2
By default, Sweet Moon will automatically identify all these elements and find the best possible combination for them. Usually, the only parameter you might want to set manually is the shared_object
. To understand shared_object
and api_reference
, check Custom Shared Objects.
The interpreter
describes which version of Sweet Moon's internal Interpreter will handle the interactions with the Lua state. The internal interpreter abstracts the Lua C API to provide methods like state.eval
, state.get
, etc.
Sweet Moon may not have an interpreter for all Lua versions, especially the too old or very specific ones. For this scenario, an error will be raised:
require 'sweet-moon'
SweetMoon::State.new(shared_object: '/usr/lib/liblua3.so')
# => SweetMoon::Errors::SweetMoonError
# No compatible interpreter found for Lua C API 3.2.2
To check all available Interpreters, you can:
require 'sweet-moon'
SweetMoon.meta.interpreters
# => ['5.0', '5.1', '5.4']
You can also check information about a state with:
require 'sweet-moon'
state = SweetMoon::State.new
state.meta.shared_objects # => ['/usr/lib/liblua.so.5.4.4']
state.meta.api_reference # => 5.4.2
state.meta.interpreter # => 5.4
state.meta.runtime # => Lua 5.4
state.meta.to_h
# => { shared_objects: ['/usr/lib/liblua.so.5.4.4'],
# api_reference: '5.4.2',
# interpreter: '5.4',
# runtime: 'Lua 5.4' }
The same is true for the global state with SweetMoon.global.state.meta
.
Exchanging Data
eval and load
The eval
method evaluates a Lua source code, and the load
method loads a file and evaluates its content. Both return the output of the evaluation if it exists.
Caveat: The data exchange works through Lua global variables only.
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('return 2 + 2') # => 4
-- source.lua
from_lua = "Lua Text"
return { data = from_ruby }
require 'sweet-moon'
state = SweetMoon::State.new
state.set('from_ruby', 'Ruby Text')
state.load('source.lua') # => { 'data' => 'Ruby Text' }
state.get('from_lua') # => 'Lua Text'
Primitives
With get
and set
, you can exchange between Lua and Ruby the following primitive types:
- Lua:
string
,integer
,number
,boolean
, andnil
. - Ruby:
String
,Symbol
,Integer
,Float
,TrueClass
(true
),FalseClass
(false
), andNilClass
(nil
).
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('lua_value = "Lua Text"') # => nil
state.get('lua_value') # => 'Lua Text'
state.set(:ruby_value, 'Ruby Text') # => nil
state.eval('return ruby_value') # => 'Ruby Text'
Caveats:
- Ruby
Symbol
(e.g.:value
) is converted to Luastring
. - Floating-point arithmetic may be tricky when exchanging numbers between two different environments.
Tables, Arrays, and Hashes
You can exchange Array
, Hash
and table
with get
and set
.
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('lua_value = {a = "text", b = 1.5, c = true}') # => nil
state.get(:lua_value) # => { 'a' => 'text', 'b' => 1.5, 'c' => true }
state.eval('list = {"a", "b", "c"}') # => nil
state.get('list') # => ['a', 'b', 'c']
state.eval('empty = {}') # => nil
state.get(:empty) # => { }
state.set('ruby_array', [3, 'a', true]) # => nil
state.eval('return ruby_array[1]') # => 3
state.eval('return ruby_array[2]') # => 'a'
state.set('ruby_hash', { a: 'b', values: ['c', 'd'] }) # => nil
state.eval('return ruby_hash["values"][2]') # => 'd'
With get
, you can use a second parameter to read a field:
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('lua_value = {a = "text", b = 1.5, c = true}') # => nil
state.get(:lua_value, :b) # => 1.5
With set
, you can use a second parameter to set a field:
require 'sweet-moon'
state = SweetMoon::State.new
state.set(:myTable, {}) # => nil
state.set(:myTable, :a, 3) # => nil
state.eval('return myTable["a"]') # => 3
Caveats:
- Ruby
Symbol
(e.g.:value
) is converted to Luastring
. - Ruby
Hash
is converted to Luatable
. - Ruby
Array
is converted to a sequential Luatable
. - Lua sequential
table
is converted to RubyArray
. - Lua non-sequential
table
is converted to RubyHash
. - Lua empty
table
is converted toHash
({}
). - Lua sequential
table
(array) starts at index1
.
Functions
Lua Functions are converted to Ruby Lambdas, where the first parameter is an array of parameters, and the second is an optional expected number of results that default to 1 (Lua Functions can return multiple results).
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('lua_fn = function(a, b) return "ok", a + b; end') # => nil
lua_fn = state.get(:lua_fn)
lua_fn.call([1, 2]) # => 'ok'
lua_fn.call([1, 2], 2) # => ['ok', 3]
lua_fn.([1, 2]) # => 'ok'
lua_fn.([1, 2], 2) # => ['ok', 3]
state.eval('second = function(list) return list[2]; end') # => nil
second = state.get(:second)
second.([%w[a b c]]) # => 'b'
Alternatively, you can send the outputs
parameter:
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('return "a", "b"', { outputs: 2 }) # => ['a', 'b']
You can call Ruby Lambdas from Lua as well:
require 'sweet-moon'
state = SweetMoon::State.new
ruby_fn = lambda do |a, b|
return a + b
end
state.set(:rubyFn, ruby_fn) # => nil
state.eval('return rubyFn(2, 2)') # => 4
sum_list = -> (list) { list.sum }
state.set('sumList', sum_list) # => nil
state.eval('return sumList({2, 3, 5})') # => 10
Other Types
We encourage you to keep a clean and simple exchange between Lua and Ruby, avoiding complex data types and bloated data structures.
Anytime you try to exchange an unsupported data type, you won't get an error, but it will be converted to a string representation:
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('return coroutine.create(function() end)')
# => 'thread: 0x93924850822056'
state.set('ruby_thread', Thread.new { 1 + 1 })
state.eval('return ruby_thread') # => '#<Thread:0x0000000000000d0c>'
Also, avoid exchanging complex things unnecessarily, e.g., modules:
require 'sweet-moon'
state = SweetMoon::State.new
state.require_module('fennel')
state.get(:fennel) # => {...}
# => It returns a huge chunk of data with
# a complex structure and mixed data types.
# It will work, but we encourage you to
# avoid that.
Prefer instead to extract what you need only:
require 'sweet-moon'
state = SweetMoon::State.new
state.require_module('fennel')
fennel_eval = state.get(:fennel, :eval)
fennel_eval.(['(+ 1 1)']) # => 2
You can also abstract what you need into global variables:
require 'sweet-moon'
state = SweetMoon::State.new
state.require_module('fennel')
state.eval('fennel_eval = fennel.eval')
fennel_eval = state.get(:fennel_eval)
fennel_eval.(['(+ 1 1)']) # => 2
Lua Global vs Local Variables
You can't exchange local variables, only global ones:
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('lua_value = "Lua Text"') # => nil
state.get('lua_value') # => 'Lua Text'
state.eval('local lua_b = "b"') # => nil
state.get('lua_b') # => nil
destroy and clear
You can destroy a state:
require 'sweet-moon'
state = SweetMoon::State.new
state.set(:a, 1)
state.get(:a) # => 1
state.destroy
state.get(:a)
# => SweetMoon::Errors::SweetMoonError
# The state no longer exists.
You can also clear a state:
require 'sweet-moon'
state = SweetMoon::State.new
state.set(:a, 1)
state.get(:a) # => 1
state.clear
state.get(:a) # => nil
Modules, Packages and LuaRocks
Check the Modules documentation at the Lua Manual to understand the essentials.
You can achieve everything through eval:
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('package.path = "/my-modules/?.lua;" .. package.path')
state.eval('package.cpath = "/my-modules/?.so;" .. package.cpath')
state.eval('some_package = require("my_module")')
Regardless, we offer some helpers that you can use.
Adding a path to the Lua package.path
:
require 'sweet-moon'
state = SweetMoon::State.new
state.add_package_path('/home/me/my-lua-modules/?.lua')
state.add_package_path('/home/me/my-lua-modules/?/init.lua')
state.add_package_cpath('/home/me/my-lua-modules/?.so')
state.add_package_path('/home/me/fennel/?.lua')
state.add_package_cpath('/home/me/?.so')
state.package_path
# => ['./?.lua',
# './?/init.lua',
# '/home/me/my-lua-modules/?.lua',
# '/home/me/my-lua-modules/?/init.lua',
# '/home/me/fennel/?.lua']
state.package_cpath
# => ['./?.so',
# '/home/me/my-lua-modules/?.so',
# '/home/me/?.so']
Requiring a module:
require 'sweet-moon'
state = SweetMoon::State.new
state.require_module('supernova')
state.require_module_as('fennel', 'f')
You can set packages in State constructors:
require 'sweet-moon'
SweetMoon::State.new(
package_path: '/folder/lib/?.lua',
package_cpath: '/lib/lib/?.so',
)
Also, you can add packages through the global config:
require 'sweet-moon'
SweetMoon.global.config(
package_path: '/folder/lib/?.lua',
package_cpath: '/lib/lib/?.so',
)
Integration with LuaRocks:
Read more about how to use LuaRocks in the official documentation: Using LuaRocks
LuaRocks is a popular package manager for the Lua language.
You can install modules like supernova with:
luarocks install supernova --local
You can figure out the path for LuaRocks modules with:
luarocks path
# => export LUA_PATH='.../home/me/.luarocks/share...
If you set the LUA_PATH
and LUA_CPATH
environment variable on your system, modules installed through LuaRocks will just work.
Alternatively, you can add it manually to the package
:
require 'sweet-moon'
state = SweetMoon::State.new
state.add_package_path('/home/me/.luarocks/share/lua/5.4/?.lua')
state.add_package_path('/home/me/.luarocks/share/lua/5.4/?/init.lua')
state.add_package_cpath('/home/me/.luarocks/lib/lua/5.4/?.so')
state.require_module('supernova')
state.eval('return supernova.enabled') # => true
state.require_module_as('supernova', 'sn')
state.eval('return sn.active_theme') # => 'default'
puts state.eval('return sn.red("hello")') # => "\e[31mhello\e[0m"
puts state.eval('return sn.blue("hello")') # => "\e[34mhello\e[0m"
You can also use the constructor:
require 'sweet-moon'
state = SweetMoon::State.new(
package_path: [
'/home/me/.luarocks/share/lua/5.4/?.lua',
'/home/me/.luarocks/share/lua/5.4/?/init.lua'
],
package_cpath: '/home/me/.luarocks/lib/lua/5.4/?.so'
)
For global:
require 'sweet-moon'
SweetMoon.global.config(
package_path: [
'/home/me/.luarocks/share/lua/5.4/?.lua',
'/home/me/.luarocks/share/lua/5.4/?/init.lua'
],
package_cpath: '/home/me/.luarocks/lib/lua/5.4/?.so'
)
Fennel
Fennel is a programming language that brings together the speed, simplicity, and reach of Lua with the flexibility of a lisp syntax and macro system.
Fennel Usage
Everything described for Lua is equivalent to Fennel, and you have the same capabilities, methods, and data exchanging.
The only thing needed is to prefix your calls with .fennel
and ensure that the Fennel module is available:
require 'sweet-moon'
state = SweetMoon::State.new
state.fennel.eval('(+ 1 2)') # => 3
state.fennel.eval('(set _G.mySum (fn [a b] (+ a b)))')
state.fennel.eval('(_G.mySum 2 3)') # => 5
mySum = state.fennel.get(:mySum)
mySum.([4, 5]) # => 9
sum_list = -> (list) { list.sum }
state.set('sumList', sum_list) # => nil
state.fennel.eval('(_G.sumList [2 3 5])') # => 10
state.fennel.load('file.fnl')
Alternatively:
require 'sweet-moon'
state = SweetMoon::State.new.fennel
state.eval('(+ 1 2)') # => 3
Fennel Global vs Local Variables
Fennel encourages you to explicitly use the _G
table to access global variables:
require 'sweet-moon'
fennel = SweetMoon::State.new.fennel
fennel.eval('(set _G.a? 2)')
fennel.get('a?') # => 2
fennel.get('_G', 'a?') # => 2
fennel.set('b', 3)
fennel.eval('(print _G.b)') # => 3
Although older versions have the expression (global name "value")
, it's deprecated, and you should avoid using that. Sweet Moon has no commitments in supporting this deprecated expression, and you should prefer the _G
way.
As is true for Lua, you can't exchange local variables, only global ones:
require 'sweet-moon'
fennel = SweetMoon::State.new.fennel
fennel.eval('(local name "value")')
fennel.get('name') # => nil
fennel.eval('(set _G.name "value")')
fennel.get('name') # => "value"
fennel.set('var-b', 35) # => nil
fennel.eval('var-b') # => nil
fennel.eval('_G.var-b') # => 35
allowedGlobals and options
As Lua, Fennel functions may return multiple results, so eval
and load
accept a second parameter to indicate the expected number of outputs:
; source.fnl
(fn multi [] (values "c" "d"))
(multi)
require 'sweet-moon'
fennel = SweetMoon::State.new.fennel
fennel.eval('(values "a" "b")', 2) # => ['a', 'b']
fennel.load('source.fnl', 2) # => ['c', 'd']
The Fennel API offers some options that eval
and load
accept as a third parameter:
require 'sweet-moon'
fennel = SweetMoon::State.new.fennel
fennel.eval('(print (+ 2 3))', 1, { allowedGlobals: ['print'] }) # => 5
fennel.eval('(print (+ 2 3))', 1, { allowedGlobals: [] })
# Compile error in unknown:1 (SweetMoon::Errors::LuaRuntimeError)
# unknown identifier in strict mode: print
# (print (+ 2 3))
# ^^^^^
# * Try looking to see if there's a typo.
# * Try using the _G table instead, eg. _G.print if you really want a global.
# * Try moving this code to somewhere that print is in scope.
# * Try binding print as a local in the scope of this code.
Alternatively, you can use the second parameter for options as well:
require 'sweet-moon'
fennel = SweetMoon::State.new.fennel
fennel.eval('(print (+ 2 3))', { allowedGlobals: ['print'] }) # => 5
You can also specify the expected outputs in the options parameter (it will be removed and not forwarded to Fennel):
require 'sweet-moon'
fennel = SweetMoon::State.new.fennel
fennel.eval(
'(values "a" "b")',
{ allowedGlobals: ['values'], outputs: 2 }
) # => ['a', 'b']
Fennel Setup
To ensure that the Fennel module is available, you can set up the LuaRocks integration or manually add the package_path
for the module.
You can download the fennel.lua
file on the Fennel's website.
Manually:
require 'sweet-moon'
state = SweetMoon::State.new
state.add_package_path('/folder/fennel/?.lua')
state.fennel.eval('(+ 1 1)') # => 2
With the constructor:
require 'sweet-moon'
fennel = SweetMoon::State.new(package_path: '/folder/fennel/?.lua').fennel
fennel.eval('(+ 1 1)') # => 2
With global:
require 'sweet-moon'
SweetMoon.global.state.add_package_path('/folder/fennel/?.lua')
SweetMoon.global.state.fennel.eval('(+ 1 1)') # => 2
Alternatively:
require 'sweet-moon'
SweetMoon.global.config(package_path: '/folder/fennel/?.lua')
SweetMoon.global.state.fennel.eval('(+ 1 1)') # => 2
Integration with fnx
fnx is a package manager for the Fennel language.
After installing fnx
and configuring it for Embedding, you can:
require 'sweet-moon'
fennel = SweetMoon::State.new.fennel
fennel.eval('(let [fnx (require :fnx)] (fnx.bootstrap!))')
Done. It will automatically inject all your dependencies according to your .fnx.fnl
file, similar to using the fnx
command.
To enforce the path for the .fnx.fnl
file:
fennel.eval('(let [fnx (require :fnx)] (fnx.bootstrap! "/project/.fnx.fnl"))')
Fennel REPL
In Ruby, you can start a REPL at any time somewhere in your code with pry:
require 'pry'
binding.pry
The same is true for Fennel, you just need to:
(let [fennel (require :fennel)]
(fennel.repl {}))
Fennel's REPL won't have your local values. But, you can tweak it to receive values to be checked inside the REPL:
(fn my-repl [to-expose]
(let [fennel (require :fennel) env _G]
(each [key value (pairs to-expose)] (tset env key value))
(fennel.repl {:env env})))
(local value "some value")
(my-repl {:value value})
; Inside the REPL:
; >> value
; "some value"
You can install readline for a better experience, e.g., autocompleting.
Check Fennel's documentation to learn more about the REPL.
Global vs Isolated
You can use the global helper that provides an API and a State for quick-and-dirty coding. It uses internally a Ruby Singleton:
require 'sweet-moon'
SweetMoon.global.state.eval('return 1 + 1')
SweetMoon.global.api.luaL_newstate
You can configure global with:
require 'sweet-moon'
SweetMoon.global.config(
shared_object: '/usr/lib/liblua.so.5.4.4',
api_reference: '5.4.2',
interpreter: '5.4'
)
To clean up, you can:
require 'sweet-moon'
SweetMoon.global.clear
As the API is just a stateless binding to the Lua C API, you can use it without worries.
You may want to use an isolated API for scenarios like interacting with two Lua versions simultaneously:
require 'sweet-moon'
api_5 = SweetMoon::API.new(shared_object: '/usr/lib/liblua5.s')
api_3 = SweetMoon::API.new(shared_object: '/usr/lib/liblua3.so')
api_5.luaL_newstate
api_3.luaH_new
Check the caveats related to Global FFI when working with multiple versions.
On the other hand, using the global State may lead to a lot of issues. You need to consider from simple things – "If I load two different files, the first file may impact the state of the second one?" – to more complex ones like multithreading, concurrency, etc.
So, you can at any time create a new isolated State and destroy it when you don't need it anymore:
require 'sweet-moon'
state = SweetMoon::State.new
state.eval('return 3 + 4') # => 7
state.load('file.lua') # => {...}
state.destroy
It's possible to empty a state with clear.
Like the API, you may want to use an isolated State to run Lua code in different Lua Versions simultaneously:
require 'sweet-moon'
state_5 = SweetMoon::State.new(shared_object: '/usr/lib/liblua5.s')
state_3 = SweetMoon::State.new(shared_object: '/usr/lib/liblua3.so')
state_5.eval('return _VERSION') # => Lua 5.4
state_3.eval('return _VERSION') # => Lua 3.2
Check the caveats related to Global FFI when working with multiple versions.
Global FFI
Some Lua libraries (e.g., readline and luafilesystem) require the Lua C API functions available in the global C environment.
By default, Sweet Moon enables Global FFI to reduce friction when using popular libraries.
Using distinct Lua versions simultaneously with multiple Shared Objects may be dangerous in this setup: Two APIs with the same name functions could be an issue because something will be overwritten.
Also, libraries that need Lua C API functions are compiled for a specific Lua version. If you are, e.g., using LuaJIT and your library expects the Standard Lua, you may face issues.
You can disable Global FFI at any time with:
require 'sweet-moon'
SweetMoon.global.config(global_ffi: false)
SweetMoon::State.new(global_ffi: false)
SweetMoon::API.new(global_ffi: false)
To check if it's enabled or not:
require 'sweet-moon'
SweetMoon.global.api.meta.global_ffi # => true
SweetMoon.global.state.meta.global_ffi # => true
SweetMoon::API.new.meta.global_ffi # => true
SweetMoon::State.new.meta.global_ffi # => true
Caveats:
Binding globally a C API is irreversible, so if you start something with global_ffi: true
and then change to global_ffi: false
, it won't make the global one disappear. If you need local, ensure that you do it from the first line and never put anything as global throughout the entire program life cycle.
Also, the simple action of accessing meta.global_ff
will bind the API, so you need to set your desired configuration before checking.
Error Handling
These are – hopefully – all the possible errors:
SweetMoonError # inherits from StandardError
LuaError # inherits from SweetMoonError
# inherits from LuaError:
LuaRuntimeError
LuaMemoryAllocationError
LuaMessageHandlerError
LuaSyntaxError
LuaFileError
You can handle the errors from the SweetMoon::Errors
namespace:
require 'sweet-moon'
begin
SweetMoon.global.state.eval('return 1 + true')
rescue SweetMoon::Errors::LuaRuntimeError => error
puts error.message
# => [string "return 1 + true"]:1: attempt to perform arithmetic on a boolean value
end
Or you can include the errors for a cleaner version with sweet-moon/errors
:
require 'sweet-moon'
require 'sweet-moon/errors'
begin
SweetMoon.global.state.eval('return 1 + true')
rescue LuaRuntimeError => error
puts error.message
# => [string "return 1 + true"]:1: attempt to perform arithmetic on a boolean value
end
Ruby feat. Lua Errors
Lua errors can be rescued inside Ruby:
-- source.lua
error('error from lua')
require 'sweet-moon'
require 'sweet-moon/errors'
state = SweetMoon::State.new
begin
state.load('source.lua')
rescue LuaRuntimeError => e
puts e.message
# => source.lua:2: error from lua
end
Ruby errors can be handled inside Lua:
require 'sweet-moon'
state = SweetMoon::State.new
state.set(:rubyFn, -> { raise 'error from ruby' })
state.load('source.lua')
-- source.lua
local status, err = pcall(rubyFn)
print(status) -- => false
print(err)
-- [string " return function (...)..."]:5: RuntimeError: error from ruby stack traceback:
-- [string " return function (...)..."]:5: in function 'rubyFn'
-- [C]: in function 'pcall'
-- source.lua:2: in main chunk
Ruby errors not handled inside Lua can be rescued inside Ruby again, with an additional Lua backtrace:
-- source.lua
a = 1
rubyFn()
require 'sweet-moon'
state = SweetMoon::State.new
state.set(:rubyFn, -> { raise 'error from ruby' })
begin
state.load('source.lua')
rescue RuntimeError => e
puts e.message # => error from ruby
puts e.backtrace.last
# => source.lua:4: in main chunk
end
Lua errors inside Lua functions can be rescued inside Ruby:
-- source.lua
function luaFn()
error('lua function error')
end
require 'sweet-moon'
require 'sweet-moon/errors'
state = SweetMoon::State.new
state.load('source.lua')
lua_fn = state.get(:luaFn)
begin
lua_fn.()
rescue LuaRuntimeError => e
puts e.message # => "source.lua:3: lua function error"
end
For Fennel, all the examples above are equally true, with additional stack traceback as well.
Where can I find .so files?
Due to the Lua's popularity, you likely have it already on your system, and Sweet Moon will be able to find the files by itself.
Either way, you can download it from:
Low-Level C API
- The API
- Custom Shared Objects
- Custom API References
- Functions, Macros and Signatures
- Low-Level C API Example
- Lua 5.4
- Lua 4.0
The API
You can access a global instance of the low-level C API with:
require 'sweet-moon'
SweetMoon.global.api
For a fresh new non-global instance:
require 'sweet-moon'
api = SweetMoon::API.new
Informations about the API:
api.meta.shared_objects # => ['/usr/lib/liblua.so.5.4.4']
api.meta.api_reference # => '5.4.2'
api.meta.to_h
# => { shared_objects: ['/usr/lib/liblua.so.5.4.4'],
# api_reference: '5.4.2' }
Custom Shared Objects
To learn more about Shared Objects and
.so
files: Dynamic linking, Dynamic linker and Executable and Linkable Format.
By default, Sweet Moon will try to find and identify the Shared Object with the highest version available. You can customize it through:
require 'sweet-moon'
api = SweetMoon::API.new(shared_object: '/usr/lib/liblua.so.5.4.4')
For the global instance:
require 'sweet-moon'
SweetMoon.global.config(shared_object: '/usr/lib/liblua.so.5.4.4')
SweetMoon.global.api
Important to notice that the API Reference will not always be the same version of the Shared Object:
require 'sweet-moon'
api = SweetMoon::API.new(shared_object: '/usr/lib/liblua.so.5.4.4')
api.meta.api_reference # => "5.4.2"
The Shared Object is from Lua 5.4.4, and the API Reference is from Lua 5.4.2.
This happens because it is impossible to extract function signatures from Shared Objects. So, Sweet Moon will use an API Reference with the highest proportion of expected functions detected in the Shared Object as a reference.
A difference in versions, for practical purposes, is not a problem, given that Sweet Moon has several relevant versions to choose from.
Custom API References
You can force an specific API Reference for your Shared Object:
require 'sweet-moon'
api = SweetMoon::API.new(
shared_object: '/usr/lib/liblua.so.5.4.4',
api_refence: '3.2.2'
)
api.meta.shared_objects # => ['/usr/lib/liblua.so.5.4.4']
api.meta.api_reference # => '3.2.2'
To check all available API References you can:
require 'sweet-moon'
SweetMoon.meta.api_references
# => ['3.2.2', '4.0.1', '5.0.3', '5.1.4', '5.4.2']
Sweet Moon won't raise errors by you trying to use an API Reference different from the Shared Object, but it will only attach valid functions, so you need to know what you are doing:
require 'sweet-moon'
api_5 = SweetMoon::API.new(shared_object: '/usr/lib/liblua.so.5.4.4')
api_3 = SweetMoon::API.new(shared_object: '/usr/lib/liblua.so.3.2.2')
api_5_with_3 = SweetMoon::API.new(
shared_object: '/usr/lib/liblua.so.5.4.4',
api_reference: '3.2.2'
)
api_5.functions.size # => 159
api_3.functions.size # => 162
api_5_with_3.functions.size # => 20
Functions, Macros, and Signatures
Sweet Moon will provide the available Lua-related functions for a Shared Object:
require 'sweet-moon'
api = SweetMoon::API.new(shared_object: '/usr/lib/liblua.so.5.4.4')
api.functions.size # 159
api.functions[0] # => :luaL_buffinitsize
api.functions[1] # => :luaL_prepbuffsize
api.functions[2] # => :luaL_checklstring
To check the signature of a function you can:
api.signature_for(:luaL_checklstring)
# => { source: 'LUALIB_API const char *(luaL_checklstring) (lua_State *L, int arg, size_t *l);',
# input: %i[pointer int pointer],
# output: :pointer }
api.signature_for(:luaL_newstate)
# => { source: 'LUALIB_API lua_State *(luaL_newstate) (void);',
# input: [],
# output: :pointer }
api.signature_for(:lua_pop)
# => { source: '#define lua_pop(L,n) lua_settop(L, -(n)-1)',
# macro: true,
# requires: [
# { source: 'LUA_API void (lua_settop) (lua_State *L, int idx);',
# input: %i[pointer int],
# output: :void }
# ] }
Notice that lua_pop
is a macro, so the information about its signature is described differently.
Low-Level C API Example
Working at a low-level with Lua will differ from version to version, and I recommend the book Programming in Lua according to your target version. Chapters related to "C API" are what you will probably search for, and the Lua Reference Manual is also a great source of information.
Lua 5.4
As an example, following-ish this reference, to get the result of the expression math.pow(2, 3)
, you would do something like:
require 'sweet-moon'
api = SweetMoon::API.new(shared_object: '/usr/lib/liblua.so.5.4.4')
state = api.luaL_newstate
api.luaL_openlibs(state)
api.luaL_loadstring(state, 'return math.pow(2, 3);')
api.lua_pcall(state, 0, 1, 0)
result = api.lua_tonumber(state, -1)
api.lua_pop(state)
api.lua_close(state)
puts result # => 8.0
This is a minimal example and does not consider things you probably should for production-ready purposes, like error handling, available stack space, type checking, etc.
Lua 4.0
As an example, following the manual, to get the result of the expression 2 ^ 3
, you would do something like:
require 'sweet-moon'
api = SweetMoon::API.new(
shared_objects: ['/usr/lib/liblua4.so', '/usr/lib/liblualib4.so']
)
state = api.lua_open(0)
api.lua_mathlibopen(state)
api.lua_dostring(state, 'return 2 ^ 3')
result = api.lua_tonumber(state, -1)
api.lua_settop(state, -2)
api.lua_close(state)
puts result # => 8.0
Notice that two Shared Objects were necessary for this Lua version, one for the Standard API and another for the Standard Libraries.
This is a minimal example and does not consider things you probably should for production-ready purposes, like error handling, available stack space, type checking, etc.
Development
bundle
rubocop -a
rspec
Tests Setup
To setup tests:
cp config/tests.sample.yml config/tests.yml
Clone the sweet-moon-test repo somewhere:
git clone git@github.com:gbaptista/sweet-moon-test.git
Update the config/tests.yml
accordingly.
Alternatively: Find or build the Shared Objects for your Operating System on your own.
Install the expected Lua rocks described in config/tests.yml
.
Running
./ports/in/shell/sweet-moon version
bundle exec sweet-moon version
bundle exec sweet-moon signatures /lua/lib/542 542.rb
bundle exec ruby some/file.rb
Publish to RubyGems
gem build sweet-moon.gemspec
gem signin
gem push sweet-moon-1.0.0.gem
Supporting New Versions
Download both the source code and the libraries.
Example: For Lua 5.4.2, you would download "Linux Libraries" and "Docs and Sources."
Extract everything to a folder, e.g., lua-542-source-libs
.
Run the command to extract the signatures:
bundle exec sweet-moon signatures /home/me/lua-542-source-libs 542.rb
Check the 542.rb
file for the output and then start coding.
You can use the logic/signatures
folder as a reference starting point.