blob: a450f841d2b540b63872dc420175a73fbbe49370 [file] [log] [blame]
/*
* Copyright (c) 2021, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "LuaEngine.h"
#include "BundleWrapper.h"
#include "JniUtils.h"
#include "jni.h"
#include <android-base/logging.h>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
extern "C" {
#include "lauxlib.h"
#include "lua.h"
#include "lualib.h"
}
namespace com {
namespace android {
namespace car {
namespace scriptexecutor {
namespace {
enum LuaNumReturnedResults {
ZERO_RETURNED_RESULTS = 0,
};
// Prefix for logging messages coming from lua script.
const char kLuaLogTag[] = "LUA: ";
} // namespace
ScriptExecutorListener* LuaEngine::sListener = nullptr;
LuaEngine::LuaEngine() {
// Instantiate Lua environment
mLuaState = luaL_newstate();
luaL_openlibs(mLuaState);
}
LuaEngine::~LuaEngine() {
lua_close(mLuaState);
}
lua_State* LuaEngine::getLuaState() {
return mLuaState;
}
void LuaEngine::resetListener(ScriptExecutorListener* listener) {
if (sListener != nullptr) {
delete sListener;
}
sListener = listener;
}
int LuaEngine::loadScript(const char* scriptBody) {
// As the first step in Lua script execution we want to load
// the body of the script into Lua stack and have it processed by Lua
// to catch any errors.
// More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
// If error, pushes the error object into the stack.
const auto status = luaL_dostring(mLuaState, scriptBody);
if (status) {
// Removes error object from the stack.
// Lua stack must be properly maintained due to its limited size,
// ~20 elements and its critical function because all interaction with
// Lua happens via the stack.
// Starting read about Lua stack: https://www.lua.org/pil/24.2.html
const char* error = lua_tostring(mLuaState, -1);
lua_pop(mLuaState, 1);
std::ostringstream out;
out << "Error encountered while loading the script. A possible cause could be syntax "
"errors in the script. Error: "
<< error;
sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(), "");
return status;
}
// Register limited set of reserved methods for Lua to call native side.
lua_register(mLuaState, "log", LuaEngine::scriptLog);
lua_register(mLuaState, "on_success", LuaEngine::onSuccess);
lua_register(mLuaState, "on_script_finished", LuaEngine::onScriptFinished);
lua_register(mLuaState, "on_error", LuaEngine::onError);
lua_register(mLuaState, "on_metrics_report", LuaEngine::onMetricsReport);
return status;
}
int LuaEngine::pushFunction(const char* functionName) {
// Interaction between native code and Lua happens via Lua stack.
// In such model, a caller first pushes the name of the function
// that needs to be called, followed by the function's input
// arguments, one input value pushed at a time.
// More info: https://www.lua.org/pil/24.2.html
lua_getglobal(mLuaState, functionName);
const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
if (status == 0) {
lua_pop(mLuaState, 1);
std::ostringstream out;
out << "Wrong function name. Provided functionName=" << functionName
<< " does not correspond to any function in the provided script";
sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(), "");
}
return status;
}
int LuaEngine::run() {
// Performs blocking call of the provided Lua function. Assumes all
// input arguments are in the Lua stack as well in proper order.
// On how to call Lua functions: https://www.lua.org/pil/25.2.html
// Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
int n_args = 2;
int n_results = 0;
// pushes the "debug" on top of the stack, so now "debug" is at index -1
lua_getglobal(mLuaState, "debug");
// pushes "traceback" as debug[traceback] because "debug" is the value at given index -1
lua_getfield(mLuaState, -1, "traceback");
// removing value "debug" from stack as we only need debug.traceback which is at index -1
lua_remove(mLuaState, -2);
// We need to insert err_handler (debug.traceback) before all arguments and function.
// Current indices (starting from top of stack): debug.traceback (-1), arg2 (-2), arg1 (-3 ==
// -n_args-1), function (-4 == -n_args-2) After insert (starting from top of stack): arg2 (-1),
// arg1 (-2 == -n_args), function (-3 == -n_args-1), debug.traceback (-4 == -n_args-2) so, we
// will insert error_handler at index : (-n_args - 2)
int err_handler_index = -n_args - 2;
lua_insert(mLuaState, err_handler_index);
// After lua_pcall, the function and all arguments are removed from the stack i.e. (n_args+1)
// If there is no error then lua_pcall pushes "n_results" elements to the stack.
// But in case of error, lua_pcall pushes exactly one element (error message).
// So, "error message" will be at top of the stack i.e. "-1".
// Therefore, we need to pop error_handler explicitly.
// error_handler will be at "-2" index from top of stack after lua_pcall,
// but once we pop error_message from top of stack, error_handler's new index will be "-1".
int status = lua_pcall(mLuaState, n_args, n_results, err_handler_index);
if (status) {
const char* error = lua_tostring(mLuaState, -1);
std::string s = error;
std::string delimiter = "stack traceback:";
// because actual delimiter is "\nstack traceback:\n\t"
// tried using \n and \t as part of the delimiter , but it did not work.
// also tried \\n and \\t in the delimiter, but it did not work.
// so to get error_msg string, avoided the \n in front by using dpos -1
// and for stack_traceback, avoided \n and \t in the tail by using delimiter.length() + 2.
int dpos = s.find(delimiter);
std::string error_msg = s.substr(0, dpos - 1);
std::string stack_traceback = s.substr(dpos + delimiter.length() + 2);
lua_pop(mLuaState, 2); // pop top 2 elements (error message & error handler) from the stack
std::ostringstream out;
out << "Error encountered while running the script. The returned error code=" << status
<< ". Refer to lua.h file of Lua C API library for error code definitions. Error: "
<< error_msg.c_str();
sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(),
stack_traceback.c_str());
}
lua_pop(mLuaState, 1); // pop top element (error handler) from the stack.
return status;
}
int LuaEngine::scriptLog(lua_State* lua) {
const auto n = lua_gettop(lua);
// Loop through each argument, Lua table indices range from [1 .. N] instead of [0 .. N-1].
// Negative indexes are stack positions and positive indexes are argument positions.
for (int i = 1; i <= n; i++) {
const char* message = lua_tostring(lua, i);
LOG(INFO) << kLuaLogTag << message;
}
return ZERO_RETURNED_RESULTS;
}
int LuaEngine::onSuccess(lua_State* lua) {
// Any script we run can call on_success only with a single argument of Lua table type.
if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_success can push only a single parameter from Lua - a Lua table",
"");
return ZERO_RETURNED_RESULTS;
}
// Helper object to create and populate Java PersistableBundle object.
BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
const auto status = convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &bundleWrapper);
if (!status.ok()) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
// Forward the populated Bundle object to Java callback.
sListener->onSuccess(bundleWrapper.getBundle());
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
int LuaEngine::onScriptFinished(lua_State* lua) {
// Any script we run can call on_success only with a single argument of Lua table type.
if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_script_finished can push only a single parameter from Lua - a Lua "
"table",
"");
return ZERO_RETURNED_RESULTS;
}
// Helper object to create and populate Java PersistableBundle object.
BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
const auto status = convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &bundleWrapper);
if (!status.ok()) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
// Forward the populated Bundle object to Java callback.
sListener->onScriptFinished(bundleWrapper.getBundle());
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
int LuaEngine::onError(lua_State* lua) {
// Any script we run can call on_error only with a single argument of Lua string type.
if (lua_gettop(lua) != 1 || !lua_isstring(lua, /* index = */ -1)) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_error can push only a single string parameter from Lua", "");
return ZERO_RETURNED_RESULTS;
}
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, lua_tostring(lua, /* index = */ -1),
/* stackTrace =*/"");
return ZERO_RETURNED_RESULTS;
}
int LuaEngine::onMetricsReport(lua_State* lua) {
// Any script we run can call on_metrics_report with at most 2 arguments of Lua table type.
if (lua_gettop(lua) > 2 || !lua_istable(lua, /* index =*/-1)) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_metrics_report should push 1 to 2 parameters of Lua table type. "
"The first table is a metrics report and the second is an optional "
"state to save",
"");
return ZERO_RETURNED_RESULTS;
}
// stack with 2 items: stack with 1 item:
// index -1: state_to_persist index -1: report
// index -2: report
// If the stack has 2 items, top of the stack is the state.
// If the stack only has one item, top of the stack is the report.
// Process the top of the stack. Create helper object and populate Java PersistableBundle
// object.
BundleWrapper topBundleWrapper(sListener->getCurrentJNIEnv());
// If the helper function succeeds, it should not change the stack
const auto status =
convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &topBundleWrapper);
if (!status.ok()) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
// If the script provided 1 argument, return now
if (lua_gettop(lua) == 1) {
sListener->onMetricsReport(topBundleWrapper.getBundle(), nullptr);
return ZERO_RETURNED_RESULTS;
}
// Otherwise the script provided a report and a state
// pop the state_to_persist because it has already been processed in topBundleWrapper
lua_pop(lua, 1);
// check that the second argument is also a table
if (!lua_istable(lua, /* index =*/-1)) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_metrics_report should push 1 to 2 parameters of Lua table type. "
"The first table is a metrics report and the second is an optional "
"state to save",
"");
return ZERO_RETURNED_RESULTS;
}
// process the report
BundleWrapper bottomBundleWrapper(sListener->getCurrentJNIEnv());
const auto statusBottom =
convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &bottomBundleWrapper);
if (!statusBottom.ok()) {
sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, statusBottom.error().message().c_str(), "");
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
// Top of the stack = state, bottom of the stack = report
sListener->onMetricsReport(bottomBundleWrapper.getBundle(), topBundleWrapper.getBundle());
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
} // namespace scriptexecutor
} // namespace car
} // namespace android
} // namespace com