hslua 0.9.2 → 0.9.3
raw patch · 13 files changed
+283/−19 lines, 13 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Foreign.Lua: class ToHaskellFunction a
+ Foreign.Lua: setglobal' :: String -> Lua ()
+ Foreign.Lua: toHaskellFunction :: ToHaskellFunction a => a -> HaskellFunction
+ Foreign.Lua: toHsFun :: ToHaskellFunction a => StackIndex -> a -> Lua NumResults
+ Foreign.Lua.Api: noref :: Int
+ Foreign.Lua.Api: refnil :: Int
+ Foreign.Lua.Api: wrapHaskellFunction :: Lua ()
+ Foreign.Lua.Api.Constants: noref :: Int
+ Foreign.Lua.Api.Constants: refnil :: Int
+ Foreign.Lua.Api.RawBindings: hslua_call_hs_ptr :: CFunction
+ Foreign.Lua.Util: setglobal' :: String -> Lua ()
Files
- CHANGELOG.md +19/−0
- README.md +101/−0
- hslua.cabal +2/−4
- safer-api/safer-api.c +38/−0
- safer-api/safer-api.h +2/−0
- src/Foreign/Lua.hs +3/−0
- src/Foreign/Lua/Api.hs +27/−1
- src/Foreign/Lua/Api/Constants.hsc +11/−0
- src/Foreign/Lua/Api/RawBindings.hsc +5/−1
- src/Foreign/Lua/FunctionCalling.hs +1/−1
- src/Foreign/Lua/Util.hs +37/−10
- test/Foreign/Lua/FunctionCallingTest.hs +27/−2
- test/Foreign/LuaTest.hs +10/−0
CHANGELOG.md view
@@ -1,4 +1,23 @@ ## Changelog++### 0.9.3++- Re-export more FunctionCalling helpers in `Foreign.Lua`: The typeclass+ `ToHaskellFunction` and the helper function `toHaskellFunction` are+ useful when working with functions. Importing them separately from+ `Foreign.Lua.FunctionCalling` was an unnecessary burden; they are+ therefor now re-exported by the main module.+- Export registry-relatd constants `refnil` and `noref`: The constants+ are related to Lua's registry functions (`ref` and `unref`).+- Add helper to convert functions into CFunction: A new helper+ `wrapHaskellFunction` is provided. It expects a+ HaskellImportedFunction userdata (as produced by+ `pushHaskellFunction`) on top of the stack and replaces it with a C+ function. The new function converts error values generated with+ `lerror` into Lua errors, i.e. it calls `lua_error`.+- Add utility function `setglobal'`: It works like `setglobal`, but+ works with packages and nested tables (dot-notation only).+ ### 0.9.2 - Add cabal flag 'export-dynamic': Default behavior is to include all symbols in
README.md view
@@ -14,6 +14,99 @@ [Hackage]: http://img.shields.io/hackage/v/hslua.svg +Overview+--------++[Lua](https://lua.org) is a small, well-designed, embeddable scripting language.+It has become the de-facto default to make programs extensible and is widely+used everywhere from servers over games and desktop applications up to security+software and embedded devices. This package provides Haskell bindings to Lua,+enable coders to embed the language into their programs, making them scriptable.++HsLua ships with batteries included and includes the most recent Lua version.+However, cabal flags make it easy to swap this out in favor of a Lua version+already installed on the host system. It supports the versions 5.1, 5.2, 5.3,+and LuaJIT.+++Interacting with Lua+--------------------++HsLua provides the `Lua` type to define Lua operations. The operations are+executed by calling `runLua`. A simple "Hello, World" program, using the Lua+`print` function, is given below:++``` haskell+import Foreign.Lua++main :: IO ()+main = runLua prog+ where+ prog :: Lua ()+ prog = do+ openlibs -- load lua libraries so we can use 'print'+ callFunc "print" "Hello, World!"+```++### The Lua stack++Lua's API is stack-centered: most operations involve pushing values to the stack+or receiving items from the stack. E.g., calling a function is performed by+pushing the function onto the stack, followed by the function arguments in the+order they should be passed to the function. The API function `call` then+invokes the function with given numbers of arguments, pops the function and+parameters of the stack, and pushes the results.++ ,----------.+ | arg 3 |+ +----------++ | arg 2 |+ +----------++ | arg 1 |+ +----------+ ,----------.+ | function | call 3 1 | result 1 |+ +----------+ ===========> +----------++ | | | |+ | stack | | stack |+ | | | |++Manually pushing and pulling arguments can become tiresome, so HsLua makes+function calling simple by providing `callFunc`. It uses type-magic to allow+different numbers of arguments. Think about it as having the signature++ callFunc :: String -> a1 -> a2 -> … -> res++where the arguments `a1, a2, …` must be of a type which can be pushed to the Lua+stack, and the result-type `res` must be constructable from a value on the Lua+stack.++### Getting values from and to the Lua stack++Conversion between Haskell and Lua values is governed by two type classes:++``` haskell+-- | A value that can be read from the Lua stack.+class FromLuaStack a where+ -- | Check if at index @n@ there is a convertible Lua value and+ -- if so return it. Throws a @'LuaException'@ otherwise.+ peek :: StackIndex -> Lua a+```++and++``` haskell+-- | A value that can be pushed to the Lua stack.+class ToLuaStack a where+ -- | Pushes a value onto Lua stack, casting it into meaningfully+ -- nearest Lua type.+ push :: a -> Lua ()+```++Many basic data types (except for numeric types, see the FAQ) have instances for+these type classes. New instances can be defined for custom types using the+functions in `Foreign.Lua.Api` (also exported in `Foreign.Lua`).++ Build flags ----------- @@ -60,6 +153,14 @@ FAQ ---++**Is anybody using this?** Absolutely. E.g., [Pandoc](https://pandoc.org), the+universal document converter, is written in Haskell and includes a Lua+interpreter, enabling programmatic modifications of documents via Lua.+Furthermore, custom output formats can be defined via Lua scripts. This has been+used in [pandoc-scholar](https://github.com/pandoc-scholar/pandoc-scholar)+([paper](https://peerj.com/articles/cs-112/)) to allow for semantically enriched+scholarly articles. **Where are the coroutine related functions?** Yielding from a coroutine works via `longjmp`, which plays very badly with Haskell's RTS. Tests to get
hslua.cabal view
@@ -1,5 +1,5 @@ name: hslua-version: 0.9.2+version: 0.9.3 stability: beta cabal-version: >= 1.8 license: MIT@@ -27,9 +27,7 @@ README.md CHANGELOG.md COPYRIGHT- test/lua/syntax-error.lua- test/lua/error.lua- test/lua/example.lua+ test/lua/*.lua source-repository head type: git
safer-api/safer-api.c view
@@ -1,6 +1,44 @@+#include <stdio.h> #include <string.h> #include "safer-api.h" +/* *********************************************************************+ * Transforming Haskell errors to Lua errors+ * *********************************************************************/+void hslua_pushhaskellerr(lua_State *L)+{+ lua_getglobal(L, "_HASKELLERR");+}++/* Error handling */+int hslua_call_hs(lua_State *L)+{+ int nargs = lua_gettop(L);+ /* Push HaskellImportFunction and call the underlying function */+ lua_pushvalue(L, lua_upvalueindex(1));+ lua_insert(L, 1);+ lua_call(L, nargs, LUA_MULTRET);++ /* Check whether an error value was returned */+ int nres = lua_gettop(L);+ /* We signal an error on the haskell side by passing two values: the special+ * haskellerr object and the error message.+ */+ if (nres == 2) {+ hslua_pushhaskellerr(L);+ int is_err = lua_rawequal(L, 0 + 1, -1);+ lua_pop(L, 1); /* pop haskellerr used for equality test */+ if (is_err) {+ lua_remove(L, 1); /* remove returned haskellerr */+ return lua_error(L);+ }+ }+ return nres;+}++/* *********************************************************************+ * Transforming Lua errors to Haskell errors+ * *********************************************************************/ /* compare */ #if LUA_VERSION_NUM >= 502 int hslua__compare(lua_State *L)
safer-api/safer-api.h view
@@ -1,5 +1,7 @@ #include "lua.h" +int hslua_call_hs(lua_State *L);+ #if LUA_VERSION_NUM >= 502 int hslua_compare(lua_State *L, int index1, int index2, int op); #endif
src/Foreign/Lua.hs view
@@ -50,6 +50,8 @@ -- * Calling Functions , PreCFunction , HaskellFunction+ , ToHaskellFunction (..)+ , toHaskellFunction , callFunc , newCFunction , freeCFunction@@ -59,6 +61,7 @@ , runLua , runLuaEither , getglobal'+ , setglobal' -- * API , module Foreign.Lua.Api , module Foreign.Lua.Api.Types
src/Foreign/Lua/Api.hs view
@@ -51,6 +51,8 @@ -- ** Constants and pseudo-indices , multret , registryindex+ , noref+ , refnil , upvalueindex -- ** State manipulation , LuaState (..)@@ -169,6 +171,7 @@ -- * Helper functions , throwTopMessageAsError , throwTopMessageAsError'+ , wrapHaskellFunction ) where import Prelude hiding (EQ, LT, compare, concat)@@ -237,6 +240,16 @@ BC.unpack <$> tostring (-1) else pop 1 *> showPointer +-- | Convert a Haskell function userdata object into a CFuntion. The userdata+-- object must be at the top of the stack. Errors signaled via lerror are+-- converted to lua errors.+wrapHaskellFunction :: Lua ()+wrapHaskellFunction = do+ t <- ltype (-1)+ case t of+ TypeUserdata -> pushcclosure hslua_call_hs_ptr 1+ _ -> throwLuaError "Need HaskellImportedFunction to create a CFunction."+ -- -- API functions --@@ -951,7 +964,20 @@ rawseti :: StackIndex -> Int -> Lua () rawseti k m = liftLua $ \l -> lua_rawseti l k (fromIntegral m) --- | See <https://www.lua.org/manual/5.3/manual.html#luaL_ref luaL_ref>.+-- | Creates and returns a reference, in the table at index @t@, for the object+-- at the top of the stack (and pops the object).+--+-- A reference is a unique integer key. As long as you do not manually add+-- integer keys into table @t@, @ref@ ensures the uniqueness of the key it+-- returns. You can retrieve an object referred by reference @r@ by calling+-- @rawgeti t r@. Function @'unref'@ frees a reference and its associated+-- object.+--+-- If the object at the top of the stack is nil, @'ref'@ returns the constant+-- @'refnil'@. The constant @'noref'@ is guaranteed to be different from any+-- reference returned by @'ref'@.+--+-- See also: <https://www.lua.org/manual/5.3/manual.html#luaL_ref luaL_ref>. ref :: StackIndex -> Lua Int ref t = liftLua $ \l -> fromIntegral <$> luaL_ref l t
src/Foreign/Lua/Api/Constants.hsc view
@@ -38,6 +38,8 @@ module Foreign.Lua.Api.Constants ( multret , registryindex+ , refnil+ , noref #if LUA_VERSION_NUMBER < 502 , environindex , globalsindex@@ -47,6 +49,7 @@ import Foreign.Lua.Api.Types #include "lua.h"+#include "lauxlib.h" -- | Alias for C constant @LUA_MULTRET@. See -- <https://www.lua.org/manual/5.3/#lua_call lua_call>.@@ -57,6 +60,14 @@ -- <https://www.lua.org/manual/5.3/#3.5 Lua registry>. registryindex :: StackIndex registryindex = StackIndex $ #{const LUA_REGISTRYINDEX}++-- | Value signaling that no reference was created.+refnil :: Int+refnil = #{const LUA_REFNIL}++-- | Value signaling that no reference was found.+noref :: Int+noref = #{const LUA_NOREF} #if LUA_VERSION_NUMBER < 502 -- | Alias for C constant @LUA_ENVIRONINDEX@. See
src/Foreign/Lua/Api/RawBindings.hsc view
@@ -41,7 +41,6 @@ import Foreign.Lua.Api.Types import Foreign.Ptr -#include "lua.h" #include "safer-api.h" -- TODO: lua_getallocf, lua_setallocf@@ -640,3 +639,8 @@ foreign import ccall safe "lauxlib.h luaL_loadstring" #endif luaL_loadstring :: LuaState -> Ptr CChar -> IO StatusCode++--------------------------------------------------------------------------------+-- * Error transformation (Haskell to Lua)+foreign import ccall "safer-api.h &hslua_call_hs"+ hslua_call_hs_ptr :: CFunction
src/Foreign/Lua/FunctionCalling.hs view
@@ -144,7 +144,7 @@ callFunc :: (LuaCallFunc a) => String -> a callFunc f = callFunc' f (return ()) 0 --- | Pushes Haskell function converted to a Lua function.+-- | Pushes Haskell function as a callable userdata. -- All values created will be garbage collected. Use as: -- -- > pushHaskellFunction myfun
src/Foreign/Lua/Util.hs view
@@ -33,6 +33,7 @@ -} module Foreign.Lua.Util ( getglobal'+ , setglobal' , runLua , runLuaEither ) where@@ -54,16 +55,42 @@ runLuaEither :: Lua a -> IO (Either LuaException a) runLuaEither = try . runLua --- | Like @getglobal@, but knows about packages. e. g.+-- | Like @getglobal@, but knows about packages and nested tables. E.g. ----- > getglobal' l "math.sin"+-- > getglobal' "math.sin" ----- returns correct result+-- will return the function @sin@ in package @math@. getglobal' :: String -> Lua ()-getglobal' n = do- let (x : xs) = splitdot n- getglobal x- mapM_ dotable xs- where- splitdot = filter (/= ".") . groupBy (\a b -> a /= '.' && b /= '.')- dotable a = getfield (-1) a *> remove (-2)+getglobal' = getnested . splitdot++-- | Like @setglobal@, but knows about packages and nested tables. E.g.+--+-- > pushstring "0.9.4"+-- > setglobal' "mypackage.version"+--+-- All tables and fields, except for the last field, must exist.+setglobal' :: String -> Lua ()+setglobal' s =+ case reverse (splitdot s) of+ [] ->+ return ()+ [_] ->+ setglobal s+ (lastField : xs) -> do+ getnested (reverse xs)+ pushvalue (-2)+ setfield (-2) lastField+ pop 1++-- | Gives the list of the longest substrings not containing dots.+splitdot :: String -> [String]+splitdot = filter (/= ".") . groupBy (\a b -> a /= '.' && b /= '.')++-- | Pushes the value described by the strings to the stack; where the first+-- value is the name of a global variable and the following strings are the+-- field values in nested tables.+getnested :: [String] -> Lua ()+getnested [] = return ()+getnested (x:xs) = do+ getglobal x+ mapM_ (\a -> getfield (-1) a *> remove (-2)) xs
test/Foreign/Lua/FunctionCallingTest.hs view
@@ -24,8 +24,9 @@ import Data.ByteString.Char8 (pack, unpack) import Foreign.Lua.Api-import Foreign.Lua.FunctionCalling (callFunc, peek, registerHaskellFunction)-import Foreign.Lua.Types (Lua, catchLuaError)+import Foreign.Lua.FunctionCalling (callFunc, peek, registerHaskellFunction,+ pushHaskellFunction)+import Foreign.Lua.Types (Lua, catchLuaError, throwLuaError) import Foreign.Lua.Util (runLua) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertEqual, testCase)@@ -74,6 +75,30 @@ errMsg = "Error during function call: could not read " ++ "argument 2: Expected a number but got a boolean" in assertEqual "Unexpected result" errMsg =<< runLua (catchLuaError luaOp (return . show))++ , testCase "convert haskell function to c function" $+ let luaOp :: Lua LuaInteger+ luaOp = do+ pushHaskellFunction integerOperation+ wrapHaskellFunction+ pushinteger 71+ pushinteger 107+ call 2 1+ peek (-1) <* pop 1+ in assertEqual "Unexpected result" 100 =<< runLua luaOp++ , testCase "Error in Haskell function is converted into Lua error" $+ let luaOp :: Lua (Bool, String)+ luaOp = do+ openlibs+ pushHaskellFunction (throwLuaError "foo" :: Lua ())+ wrapHaskellFunction+ setglobal "throw_foo"+ loadstring "return pcall(throw_foo)" *> call 0 2+ (,) <$> peek (-2) <*> peek (-1)++ errMsg = "Error during function call: foo"+ in assertEqual "Unexpected result" (False, errMsg) =<< runLua luaOp ] , testGroup "call lua function from haskell"
test/Foreign/LuaTest.hs view
@@ -83,6 +83,16 @@ pushLuaExpr "'Moin'" equal (-1) (-2) + , luaTestCase "setting a nested global works" $ do+ let v = "Mitte"+ newtable+ setglobal "berlin"++ pushstring v+ setglobal' "berlin.neighborhood"+ v' <- getglobal' "berlin.neighborhood" *> tostring (-1)+ return (v == v')+ , testCase "table reading" . runLua $ do openbase