Pull to refresh

Вызов Lua скриптов из Java и наоборот

Reading time 6 min
Views 15K
image
Недавно столкнулся с такой проблемой и был крайне удивлен, что в сети материалов по ней очень мало, учитывая популярность Lua. Как выяснилось, существует довольно много библиотек для работы с Lua-скриптами из java, но все они имеют свои нюансы. Лучше всего, как выяснилось, использовать библиотеку LuaJava от тех же разработчиков, что написали Lua.

Сборка библиотеки LuaJava

Сразу скажу, что распаковывать архив с иходниками и выполнять сборку лучше в специальной папке, откуда вы будете подключать собранный jar-файл к проекту. Путь к этой папке также нужно будет прописывать в ClassPath. Если собрать jar-архив, а потом скопировать его в папку с java-проектом, то программа просто так не запустится. Но об этом будет ниже.
На странице мануала написано, что счастливые пользователи Linux и OSX могут собрать библиотеку из исходников простой командой make, внеся незначительные изменения в конфигурационный файл — config. Изначально этот файл выглядит так
#############################################################
#Linux/BSD/Mac
LUA_DIR= /usr/local/share/lua/5.1.1
LUA_LIBDIR= /usr/local/lib
LUA_INCLUDES= /usr/local/include
JDK= $(JAVA_HOME)
# For Mac OS, comment the above line and uncomment this one
#JDK=/Library/Java/Home

# Full path to Lua static library
LIB_LUA=$(LUA_LIBDIR)/liblua.a

#Linux/BSD
LIB_EXT= .so
#Mac OS
#LIB_EXT= .jnilib

LIB_PREFIX= lib

#Linux/BSD
LIB_OPTION= -shared
#Mac OS
#LIB_OPTION= -dynamiclib -all_load

## On FreeBSD and Mac OS systems, the following line should be commented
DLLIB= -ldl

WARN= -O2 -Wall -fPIC -W -Waggregate-return -Wcast-align -Wmissing-prototypes -Wnested-externs -Wshadow -Wwrite-strings
INCS= -I$(JDK)/include -I$(JDK)/include/linux -I$(LUA_INCLUDES)
CFLAGS= $(WARN) $(INCS)

CC= gcc

#########################################################
VERSION= 1.1
PKG= luajava-$(VERSION)
TAR_FILE= $(PKG).tar.gz
ZIP_FILE= $(PKG).zip
JAR_FILE= $(PKG).jar
SO_FILE= $(LIB_PREFIX)$(PKG)$(LIB_EXT)
DIST_DIR= $(PKG)

# $Id: config,v 1.12 2006/12/22 14:06:40 thiago Exp $

Если сразу пытаться выполнить make, то ничего хорошего не произойдет. Вот что нужно сделать, чтобы библиотека успешно собралась:
1. Либо создать переменную среды JAVA_HOME, либо прописать ее в конфигурационном файле, указав путь к папке, куда установлена java. Я добавлял эту переменную первой строчкой в конфигурационном файле:
JAVA_HOME= /usr/lib/jvm/java-6-sun-1.6.0.26

2. Скачать бинарные файлы Lua отсюда. Нам нужен архив Linux###_lib.tar.gz. После распаковки архива файлы liblua5.1a и liblua5.1.so нужно скопировать в папку /usr/local/lib, а файлы из папки include — в папку /usr/local/include. После этого в конфигурационном файле заменить строчку
LIB_LUA=$(LUA_LIBDIR)/liblua.a
на
LIB_LUA=$(LUA_LIBDIR)/liblua5.1.a

3. Также должен быть установлен gcc.

Все, теперь можно собирать библиотеку. После выполнения команды make появляются 2 файла: luajava-1.1.jar и libluajava-1.1.so. В мануале написано, что .jar нужно положить в папку lib/ с вашим проектом, а файлик .so либо в папку JRE bin/, либо в папку, куда указывает переменная LD_LIBRARY_PATH. Я помещал этот файлик в папку JRE bin/.

Конфигурация java проекта

В java проекте нужно подключить внешнюю библиотеку luajava-1.1.jar. Также, чуть забегая вперед, нужно в аргументах VM добавить строку
-Djava.library.path=[путь к папке, где выполнялась сборка LuaJava]

Теперь можно спокойно писать Lua-скрипты и обращаться к ним из программы. Делается это с помощью класса org.keplerproject.luajava.LuaState. Наткнулся на пример, в котором для вызова Lua-функций был написан специальный класс. Вот пример класса, который я использую:

import org.keplerproject.luajava.LuaObject;
import org.keplerproject.luajava.LuaState;
import org.keplerproject.luajava.LuaStateFactory;

public class LuaScriptLoader
{
	private LuaState luaState;

	public LuaScriptLoader(String fileFullName)
	{
		luaState = LuaStateFactory.newLuaState();
		luaState.openLibs();
		luaState.LdoFile(fileFullName);
	}

	public void closeScript()
	{
		luaState.close();
	}
	
	/**
	 * Метод, вызывающий Lua-функцию getRoomDescription, которая возвращает описание комнаты
	 * 
	 * @return строка описания комнаты
	 */
	public String getRoomDescription()
	{
		luaState.getGlobal("getRoomDescription");
		luaState.call(0, 1);
		LuaObject lo = luaState.getLuaObject(1);
		luaState.pop(1);
		return lo.getString();
	}
	
	/**
	 * Метод, вызывающий определенную функцию в Lua-скрипте
	 * 
	 * @param functionName - имя функции
	 * @param adapter - объект LuaAdapter, описывающий параметры вызова функции
	 */
	public void runScriptFunction(String functionName, LuaAdapter adapter)
	{
		luaState.getGlobal(functionName);
		luaState.pushJavaObject(adapter);
		luaState.call(1, 0);
	}
}


Программа будет запускаться из среды программирования. Если хотите, чтобы запускался .jar-файл с вашей программой, то нужно сделать 2 вещи:
1. При экспорте использовать файл манифеста, в котором должен быть прописан атрибут Class-Path
Class-Path: [путь к папке, где выполнялась сборка LuaJava]/luajava-1.1.jar

2. Запускать файл .jar нужно с тем же ключом, что прописан в аргументах VM среды
-Djava.library.path=[путь к папке, где выполнялась сборка LuaJava]

При обращении к методу call класса LuaState передаются 2 целочисленных параметра. Это количество аргументов и количество возвращаемых значений. В методе getRoomDescription() видно, как получать результат из Lua-функции (0 аргументов, 1 возвращаемое значение), а в методе runScriptFunction() — как передавать java-объекты скрипту (1 аргумент, 0 возвращаемых значений). Класс LuaAdapter — это мой класс, у которого есть несколько публичных методов. Эти методы могут быть вызваны из скрипта. Вот пример файла со скриптом:

function getRoomDescription()
    return "Конференция"
end

function processMessage(adapter)
	local message = adapter:getMessage()
	local user = adapter:getUser()
	local nick = user:getNick()

	adapter:sendMessageToRoomAndUser(user, nick..": "..message, "")
end

function userEnter(adapter)
    local user = adapter:getUser()
    local nick = user:getNick()
    adapter:sendMessageToRoomAndUser(user, "Вошел пользователь "..nick, "Вы вошли в конференцию")
end

function userLeft(adapter)
    local nick = adapter:getUser():getNick()
    adapter:sendMessageToRoom("Пользоатель "..nick.." вышел из конференции")
end


В данном примере с помощью Lua заскриптована комната-конференция, где пользователи могут общаться. Все публичные методы, которые есть в классе LuaAdapter, успешно вызываются и выполняются.

UPD: забыл еще одну вещь. Работать с java-классами и объектами из Lua можно не только так, как я описал выше (передавая объект в качестве параметра Lua-функции). В мануале LuaJava описано еще несколько способов. С первыми двумя я работал, они работали без всяких проблем, остальные не пробовал за ненадобностью. Опишу первые два способа:
1. Вызов функции newInstance(className, ...) библиотеки LuaJava. Первым параметром функции является полное имя java-класса, остальные — параметры конструктора. Результатом вызова этой функции будет новый java-объект, с которым можно будет работать.
Пример:

obj = luajava.newInstance("java.lang.Object")


Можно также вызывать свои классы, передавая их имена в качестве параметров (например, «myproject.mypackage.MyClass»). Но здесь могут встретиться нюансы, которые мне не встретились:
  • непонятно, к какому проекту имеет доступ Lua-скрипт. Сомневаюсь, что ко всем
  • если есть доступ ко многим проектам, или вызов Lua-скриптов осуществляется из нескольких проектов, и в этих проектах есть классы с одинаковыми именами, то непонятно, объект какого именно класса будет создан


2. Вызов функции bindClass(className) библиотеки LuaJava. Эта функция возвратит объект, у которого можно будет обращаться к статическим полям и методам java-класса, имя которого было указано.
Пример:

sys = luajava.bindClass("java.lang.System")
print ( sys:currentTimeMillis() )


Также можно вызывать свои классы (с теми же нюансами).
Я вызывал эти Lua-функции только из одного проекта, и в функциях обращался к классам только этого проекта, поэтому проблем не встретил

UPD Обнаружился нюанс. Метод call не умеет отлавливать ошибки, которые могут присутствовать в скрипте. Если в скрипте ошибка, то рушится вся программа. Иксепшенов этот метод не бросает. Единственное решение, которое нагуглил — это пытаться запускать вызов скриптов из другого потока. Но это очен некрасиво. Зато у класса LuaState есть метод pcall:

public int pcall(int nArgs,
                 int nResults,
                 int errFunc)


который в случае успешного вызова вернет значение 0, и ненулевое значение в результате неуспешного вызова. Первые два аргумента повторяют аргументы метода call, последний параметр видимо должен указывать на функцию обработки ошибок. При этом описание ошибки будет сохранено в объекте LuaState, с помощью которого вызывалась скриптовая функция. Теперь метод runScriptFunction у меня выглядит так:

	public void runScriptFunction(String functionName, LuaAdapter adapter)
	{
		luaState.getGlobal(functionName);
		luaState.pushJavaObject(adapter);
		int res = luaState.pcall(1, 0, 0);
		if(res != 0)
		{
			Main.log(Level.SEVERE, "LuaScriptLoader call error: " + luaState.toString(-1));
		}
	}
}
Tags:
Hubs:
0
Comments 5
Comments Comments 5

Articles