Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ p sum.as(Lua::Function).call(3.2, 1) # => 4.2
lua.close
```

You can also expose Crystal procs to Lua as global functions. Argument and
return types are taken from the proc's signature, so values flow naturally
between the two languages:

```crystal
lua = Lua.load

lua.function "add", ->(x : Float64, y : Float64) { x + y }
lua.run "return add(3, 4)" # => 7.0

# closures capture local Crystal state
counter = 0
lua.function "tick", -> { counter += 1; nil }
lua.run "tick(); tick()"
counter # => 2

# method pointers work too
def greet(name : String)
"Hi, #{name}"
end

lua.function "greet", ->greet(String)
lua.run "return greet('Lua')" # => "Hi, Lua"

lua.close
```

More features coming soon. Try it, that's fun :)

## Contributing
Expand Down
34 changes: 34 additions & 0 deletions examples/crystal_callback.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require "../src/lua"

# crystal run examples/crystal_callback.cr

lua = Lua.load

# 1. inline proc literal
lua.function "add", ->(x : Float64, y : Float64) { x + y }
puts lua.run "return add(3, 4)" # => 7.0

# 2. closure capturing local Crystal state
counter = 0
lua.function "tick", -> { counter += 1; nil }
lua.run "tick(); tick(); tick()"
puts "counter = #{counter}" # => counter = 3

# 3. method pointer
def greet(name : String)
"Hi, #{name}"
end

lua.function "greet", ->greet(String)
puts lua.run "return greet('Lua')" # => Hi, Lua

# 4. mixed types, used inside a larger Lua expression
lua.function "join", ->(prefix : String, count : Int32, ok : Bool) do
"#{prefix}:#{count}:#{ok}"
end
puts lua.run %q{
local label = join("requests", 5, true)
return label .. " (" .. tostring(add(10, 20)) .. ")"
} # => requests:5:true (30.0)

lua.close
77 changes: 77 additions & 0 deletions spec/lua/stack/function_support_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require "../../spec_helper"

private def greet(name : String)
"Hi, #{name}"
end

module Lua::StackMixin
describe FunctionSupport do
describe "#function" do
it "registers a proc taking no arguments" do
s = Stack.new
s.function "answer", -> { 42.0 }
s.run("return answer()").should eq 42.0
end

it "registers a proc with typed numeric arguments" do
s = Stack.new
s.function "add", ->(x : Float64, y : Float64) { x + y }
s.run("return add(3, 4)").should eq 7.0
end

it "converts integer-typed arguments" do
s = Stack.new
s.function "mul", ->(x : Int32, y : Int32) { x * y }
s.run("return mul(6, 7)").should eq 42.0
end

it "accepts string arguments and returns a string" do
s = Stack.new
s.function "shout", ->(text : String) { text.upcase }
s.run("return shout('hello')").should eq "HELLO"
end

it "captures local Crystal state via closure" do
s = Stack.new
counter = 0
s.function "tick", -> { counter += 1; nil }
s.run "tick(); tick(); tick()"
counter.should eq 3
end

it "registers a method pointer" do
s = Stack.new
s.function "greet", ->greet(String)
s.run("return greet('Lua')").should eq "Hi, Lua"
end

it "returns no Lua value when the proc returns Nil" do
s = Stack.new
s.function "noop", -> { nil }
s.run("return noop()").should be_nil
end

it "is callable inside larger Lua expressions" do
s = Stack.new
s.function "double", ->(x : Float64) { x * 2 }
s.run("return 100 * double(3)").should eq 600.0
end

it "raises when Lua passes fewer arguments than expected" do
s = Stack.new
s.function "add", ->(x : Float64, y : Float64) { x + y }
expect_raises(ArgumentError, /add.*expects 2/) do
s.run "return add(1)"
end
end

it "supports several mixed-type arguments" do
s = Stack.new
s.function "join", ->(prefix : String, count : Int32, ok : Bool) do
"#{prefix}:#{count}:#{ok}"
end
s.run("return join('n', 5, true)").should eq "n:5:true"
end
end
end
end
1 change: 1 addition & 0 deletions src/lua/stack.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Lua
include StackMixin::CoroutineSupport
include StackMixin::StandardLibraries
include StackMixin::ClassSupport
include StackMixin::FunctionSupport

getter state
getter libs = Set(Symbol).new
Expand Down
58 changes: 58 additions & 0 deletions src/lua/stack/function_support.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Lua
module StackMixin::FunctionSupport
# Registers a Crystal `Proc` as a global Lua function under the given name.
#
# Argument types come from the proc's signature: each Lua argument is
# converted to the matching Crystal type before the proc is called, and
# the proc's return value is pushed back onto the Lua stack. A proc
# returning `Nil` produces no Lua return value.
#
# ```
# lua = Lua.load
#
# # inline proc literal
# lua.function "add", ->(x : Float64, y : Float64) { x + y }
# lua.run "return add(3, 4)" # => 7.0
#
# # closure over local state
# counter = 0
# lua.function "tick", -> { counter += 1; nil }
# lua.run "tick(); tick()"
# counter # => 2
#
# # method pointer
# def greet(name : String)
# "Hi, #{name}"
# end
#
# lua.function "greet", ->greet(String)
# lua.run "return greet('Lua')" # => "Hi, Lua"
# ```
def function(name : String, proc : Proc(*Args, R)) forall Args, R
libs_snapshot = libs.to_a
arity = {{ Args.type_vars.size }}
wrapper = ->(state : LibLua::State) do
if LibLua.gettop(state) < arity
raise ArgumentError.new("'#{name}' expects #{arity} argument(s)")
end
stack = Lua::Stack.new(state, libs_snapshot)
{% if Args.type_vars.empty? %}
result = proc.call
{% else %}
result = proc.call(
{% for type, i in Args.type_vars %}
::LuaCallable::LuaConvert({{type}}).convert(stack[{{i + 1}}]),
{% end %}
)
{% end %}
{% if R.resolve == Nil %}
0
{% else %}
stack << result
1
{% end %}
end
set_global(name, wrapper)
end
end
end
Loading