mscript - 2.0 版本新增错误处理、新函数和 DLL 集成





5.00/5 (2投票s)
如果您还在犹豫是否将 mscript 加入您的系统工具库,不妨再看看。
引言
本文展示了 mscript 2.0 的改进,包括 DLL 集成的工作方式、JSON 支持的集成方式以及错误处理系统。
在深入了解 2.0 版本之前,如果您不熟悉 mscript,请查看 关于 1.0 版本的 CodeProject 文章。
将 "!" 语句从注释改为错误日志记录,这是一个重大更改。
还有另一个重大更改,更像是修复性更改,即不再提供接受变量值作为函数名的“魔术”功能。是的,那是个糟糕的主意。我曾因此吃过亏。我不会让您也这样。抱歉,这不是函数式语言,也没有函数指针。我想您会适应的。
外部链接
mscript 是一个开源项目,托管在 GitHub 上。
有关语言参考,请查看 mscript.io。
错误处理
在 1.0 版本中,您会调用 error() 函数,程序会打印消息并退出。
在 2.0 版本中,您仍然调用 error()。现在您使用 "!" 语句来处理这些错误。
注意:重大更改:我将 "!" 从用于单行注释的功能中移除。现在您使用 "/" 作为单行注释。如果您觉得方便,可以使用 "//" 。;-)
这里有一个示例来演示错误处理系统
~ verifyLow(value)
? value >= 10
* error("Value is too high: " + value)
}
}
* verifyLow(13)
! err
> "verifyLow failed: " + err
}
- mscript 调用 verifyLow(13) 函数。
- verifyLow() 发现 13 >= 10。
- verifyLow() 使用字符串调用 error()。
- mscript 在 error() 调用下方查找 "!" 语句。
- 在 verifyLow() 中未找到 "!" 语句,错误会冒泡到调用代码。
- mscript 在 verifyLow() 调用之后扫描 "!" 语句。
- 找到 "!" 语句后,它会执行该 "!" 语句,并将 err 设置为 error() 被调用的任何内容。
没有 "try {} catch {}" 之类的机制。
您可以将任何内容传递给 error() 函数,并且 "!" 语句可以使用 getType() 函数和索引,因此错误处理可以像您想要的任何方式一样简单或复杂。
使用 "!" 语句,您可以为错误处理程序的变量命名,随意命名 - 不必是 err,您可以随意命名。
在 "!" 语句之前可以有任意数量的语句,而不是每个前置语句只有一个 "!"。
所以如果您有...
* do1()
* do2()
* do3()
! msg
> "I did something wrong: " + msg
}
如果 do1() 导致错误,mscript 将会扫描过 do2() 和 do3() 调用,以找到 "!" 语句。
新函数
现在有 toJson(mscriptObject) 和 fromJson(jsonString) 函数,用于将任何 mscript 对象转换为 JSON 字符串,以及将 JSON 字符串转换为 mscript 对象。结合 readFile 和 writeFile 函数,您可以拥有一个例如设置文件。
现在有 getEnv(name) 和 putEnv(name, value) 函数用于处理环境变量。putEnv() 对环境所做的更改仅在 mscript 内部可见,并且对 mscript 通过 exec() 函数生成的程序可见。这对于替换 .bat 文件应该很有用。
有 sleep(seconds)。
JSON 集成
我选择在一个单独的模块中开发新功能,而不是将 object::toJson() / object::fromJson() "烧录" 到 object 类及其头文件和源文件中,该模块有两个函数:
- objectFromJson(jsonString),以及
objectToJson(object)
头文件仅包含 object.h,JSON 代码不会污染此模块之外的任何地方。
对于 objectToJson,我手工编写了代码,因为它非常简单。唯一复杂的只是字符串中的转义字符。其余都很简单。
但是,解析 JSON 需要做一些工作。
我选择了 nlohmann 的 json.hpp,因为它集成起来非常简单,而且我看到它效果很好。
#include "../../json/single_include/nlohmann/json.hpp"
我选择了 SAX 风格的解析器以求简洁。正确实现花了一些时间。结果很干净,我没有遇到任何问题。
// We maintain two stacks: m_objStack for objects that can nest, like lists and indexes;
// and m_keyStack for managing the name of the current index key, to handle nested indexes
class mscript_json_sax
{
public:
// Start with a stack frame
mscript_json_sax()
{
m_objStack.emplace_back();
}
// Finish reading the first frame
object final() const
{
return m_objStack.front();
}
//
// SAX values interface
// Handle the little easy stuff
//
bool null()
{
set_obj_val(object());
return true;
}
bool boolean(bool val)
{
set_obj_val(val);
return true;
}
bool number_integer(json::number_integer_t val)
{
set_obj_val(double(val));
return true;
}
bool number_unsigned(json::number_unsigned_t val)
{
set_obj_val(double(val));
return true;
}
bool number_float(json::number_float_t val, const json::string_t&)
{
set_obj_val(double(val));
return true;
}
bool string(json::string_t& val)
{
set_obj_val(toWideStr(val));
return true;
}
bool binary(json::binary_t&)
{
raiseError("Binary values are not allowed in mscript JSON");
}
//
// SAX index interface
//
// Push an index onto the stack
bool start_object(std::size_t)
{
m_objStack.push_back(object::index());
return true;
}
// on_end takes care of indexes and lists
bool end_object()
{
return on_end();
}
//
// SAX list interface
//
// Push a list onto the stack
bool start_array(std::size_t)
{
m_objStack.push_back(object::list());
return true;
}
bool end_array()
{
return on_end();
}
// Push the current index key name for later retrieval
bool key(json::string_t& val)
{
m_keyStack.push_back(object(toWideStr(val)));
return true;
}
// If there is a problem parsing the JSON, use our exception raising function to punt it
bool parse_error(std::size_t pos, const std::string&,
const nlohmann::detail::exception& exp)
{
raiseError("JSON parse error at " + std::to_string(pos) +
": " + std::string(exp.what()));
//return false;
}
private:
// When we receive a new object value:
// it could be a new index value (use the m_keyStack key);
// it could be a new list value;
// or it could be the value for the main top-level object
void set_obj_val(const object& obj)
{
object& cur_obj = m_objStack.back();
if (cur_obj.type() == object::INDEX)
{
if (m_keyStack.empty())
raiseError("No object key in context");
cur_obj.indexVal().set(m_keyStack.back(), obj);
m_keyStack.pop_back();
}
else if (cur_obj.type() == object::LIST)
cur_obj.listVal().push_back(obj);
else
cur_obj = obj;
}
// When a list or index ends, we get the top of the stack, back_obj and pop the stack.
// The new top is top cur_obj.
// If cur_obj is an index or list, we add back_obj to it, using the key stack for an index.
// This would be when the back_obj was some index or list that got started
// inside of another list or index, cur_obj.
// So when back_obj is popped, it needs to find its home in cur_obj.
// If cur_obj is not index or list, its value is set to what was popped, back_obj.
bool on_end()
{
object back_obj = m_objStack.back();
m_objStack.pop_back();
object& cur_obj = m_objStack.back();
if (cur_obj.type() == object::INDEX)
{
if (m_keyStack.empty())
raiseError("No object key in context");
cur_obj.indexVal().set(m_keyStack.back(), back_obj);
m_keyStack.pop_back();
}
else if (cur_obj.type() == object::LIST)
cur_obj.listVal().push_back(back_obj);
else
cur_obj = back_obj;
return true;
}
std::vector<object> m_objStack;
std::vector<object> m_keyStack;
};
// With the SAX processor complete, implementing objectFromJson is straightforward
object mscript::objectFromJson(const std::wstring& json)
{
mscript_json_sax my_sax;
if (!json::sax_parse(json, &my_sax))
raiseError("JSON parsing failed");
object final = my_sax.final();
return final;
}
DLL 集成
借助 mscript 2.0,开发者可以编写自己的 DLL 来扩展 mscript。
引用开发 mscript DLL...
我使用的是 Visual Studio 2022 Community Edition;Visual Studio 2019 和任何版本应该都可以。
从 GitHub 克隆 mscript 解决方案。
克隆 GitHub 上的 nlohmann JSON 库,并将其放在 mscript 解决方案目录旁边,而不是内部,与它并列。
使 mscript 解决方案能够构建并使单元测试通过。
要理解 DLL 集成,最好查看 mscript-dll-sample 项目。
pch.h: #pragma once #include "../mscript-core/module.h" #pragma comment(lib, "mscript-core")这本身就包含了您进行 mscript DLL 工作所需的一切。
然后,您编写一个 dllinterface.cpp 文件来实现您的 DLL。
这是 mscript-dll-sample 的 dllinterface.cpp。
#include "pch.h" using namespace mscript; // You implement mscript_GetExports to specify which functions you will be exporting. // Your function names have to be globally unique, // and can't have dots, so use underscores and make it unique. wchar_t* __cdecl mscript_GetExports() { std::vector<std::wstring> exports { L"ms_sample_sum", L"ms_sample_cat" }; return module_utils::getExports(exports); } // You need to provide a memory freeing function for strings that your DLL allocates void mscript_FreeString(wchar_t* str) { delete[] str; } // Here's the big one. You get a function name, and JSON for a list of parameters. // module_utils::getNumberParams keeps this code pristine // Use module_utils::getParams instead for more general parameter handling. // module_utils::jsonStr(retVal) turns any object into a JSON wchar_t* // to return to mscript. // module_utils::errorStr(functionName, exp) gives you consolidated error handling, // returning an error message JSON wchar_t* that mscript expects. wchar_t* mscript_ExecuteFunction(const wchar_t* functionName, const wchar_t* parametersJson) { try { std::wstring funcName = functionName; if (funcName == L"ms_sample_sum") { double retVal = 0.0; for (double numVal : module_utils::getNumberParams(parametersJson)) retVal += numVal; return module_utils::jsonStr(retVal); } else if (funcName == L"ms_sample_cat") { std::wstring retVal; for (double numVal : module_utils::getNumberParams(parametersJson)) retVal += num2wstr(numVal); return module_utils::jsonStr(retVal); } else raiseWError(L"Unknown mscript-dll-sample function: " + funcName); } catch (const user_exception& exp) { return module_utils::errorStr(functionName, exp); } catch (const std::exception& exp) { return module_utils::errorStr(functionName, exp); } catch (...) { return nullptr; } }您可以跳出这个固定的路径。
处理参数列表 JSON 并返回映射到 mscript 值的 JSON。这就是全部的假设。
创建自己的 DLL 后,在 mscript 代码中,您使用与导入 mscript 相同的 + 语句将其导入。
DLL 的搜索路径相对于 mscript EXE 所在的文件夹,并且出于安全原因,不在其他任何地方搜索。
这就是 mscript DLL 开发的概述。但 mscript 如何与这些 DLL 一起工作?
让我们看看 module.h,了解 mscript 向 DLL 作者提供了什么。
// This header is big on convenience for the DLL author...
#pragma once
// If on Windows, bring in Windows
#if defined(_WIN32) || defined(_WIN64)
#include <Windows.h>
#endif
// Bring in all the mscript code needed to use objects
// and do JSON serialization
#include "object.h"
#include "object_json.h"
#include "utils.h"
// Define the entry points that mscript DLLs must implement
extern "C"
{
__declspec(dllexport) wchar_t* __cdecl mscript_GetExports();
__declspec(dllexport) void __cdecl mscript_FreeString(wchar_t* str);
__declspec(dllexport) wchar_t* __cdecl mscript_ExecuteFunction
(const wchar_t* functionName, const wchar_t* parametersJson);
}
// The module_utils class has all utility code for easy mscript DLL development
// wchar_t* is the output returned from mscript_ExecuteFunction's to mscript,
// so all routines are returning freshly cloned raw character buffers
// mscript receives the buffers, stashes them in objects,
// and calls the mscript_FreeString
// function to release the memory
namespace mscript
{
class module_utils
{
public:
// Core string cloning routine
static wchar_t* cloneString(const wchar_t* str);
// Helper for implementing mscript_GetExports
// Just pass in your vector of function name strings, return it,
// you're all set
static wchar_t* getExports(const std::vector<std::wstring>& exports);
// Extract parameters from the JSON passed into the function
static object::list getParams(const wchar_t* parametersJson);
static std::vector<double> getNumberParams(const wchar_t* parametersJson);
// Turn anything into a JSON string ready to be returned to mscript
static wchar_t* jsonStr(const object& obj);
static wchar_t* jsonStr(const std::string& str);
// Handle exceptions cleanly by returning the output of these functions
static wchar_t* errorStr(const std::wstring& function,
const user_exception& exp);
static wchar_t* errorStr(const std::wstring& function,
const std::exception& exp);
private:
module_utils() = delete;
module_utils(const module_utils&) = delete;
module_utils(module_utils&&) = delete;
module_utils& operator=(const module_utils&) = delete;
};
}
有了这个模块,开发 mscript DLL 应该会很直接。
在 mscript 内部,DLL 由 lib 类表示。
typedef wchar_t* (*GetExportsFunction)();
typedef void (*FreeStringFunction)(wchar_t* str);
typedef wchar_t* (*ExecuteExportFunction)(const wchar_t* functionName,
const wchar_t* parametersJson);
class lib
{
public:
lib(const std::wstring& filePath);
~lib();
const std::wstring& getFilePath() const { return m_filePath; }
object executeFunction(const std::wstring& name, const object::list& paramList) const;
static std::shared_ptr<lib> loadLib(const std::wstring& filePath);
static std::shared_ptr<lib> getLib(const std::wstring& name);
private:
#if defined(_WIN32) || defined(_WIN64)
HMODULE m_module;
#endif
std::wstring m_filePath;
FreeStringFunction m_freer;
ExecuteExportFunction m_executer;
std::unordered_set<std::wstring> m_functions;
static std::unordered_map<std::wstring, std::shared_ptr<lib>> s_funcLibs;
static std::mutex s_libsMutex;
};
您创建一个 lib,并提供 DLL 的路径,然后就可以执行该 lib 的函数了。
lib 构造函数完成了 LoadLibrary / GetProcAddress 的所有操作。
lib::lib(const std::wstring& filePath)
: m_filePath(filePath)
, m_executer(nullptr)
, m_freer(nullptr)
, m_module(nullptr)
{
m_module = ::LoadLibrary(m_filePath.c_str());
if (m_module == nullptr)
raiseWError(L"Loading library failed: " + m_filePath);
m_freer = (FreeStringFunction)::GetProcAddress(m_module, "mscript_FreeString");
if (m_freer == nullptr)
raiseWError(L"Getting mscript_FreeString function failed: " + m_filePath);
std::wstring exports_str;
{
GetExportsFunction get_exports_func =
(GetExportsFunction)::GetProcAddress(m_module, "mscript_GetExports");
if (get_exports_func == nullptr)
raiseWError(L"Getting mscript_GetExports function failed: " + m_filePath);
wchar_t* exports = get_exports_func();
if (exports == nullptr)
raiseWError(L"Getting exports from function mscript_GetExports failed: " +
m_filePath);
exports_str = exports;
m_freer(exports);
exports = nullptr;
}
std::vector<std::wstring> exports_list = split(exports_str, L",");
for (const auto& func_name : exports_list)
{
std::wstring func_name_trimmed = trim(func_name);
if (!isName(func_name_trimmed))
raiseWError(L"Invalid export from function mscript_GetExports: " +
m_filePath + L" - " + func_name_trimmed);
const auto& funcIt = s_funcLibs.find(func_name_trimmed);
if (funcIt != s_funcLibs.end())
{
if (toLower(funcIt->second->m_filePath) != toLower(m_filePath))
raiseWError(L"Function already defined in another export: function " +
func_name_trimmed + L" - in " + m_filePath + L" - already defined in " +
funcIt->second->m_filePath);
else if (m_functions.find(func_name_trimmed) != m_functions.end())
raiseWError(L"Duplicate export function: " + func_name_trimmed + L" in " +
m_filePath);
else
continue;
}
m_functions.insert(func_name_trimmed);
}
m_executer = (ExecuteExportFunction)::GetProcAddress(m_module, "mscript_ExecuteFunction");
if (m_executer == nullptr)
raiseWError(L"Getting mscript_ExecuteFunction function failed: " + m_filePath);
}
执行 DLL 函数会使用 DLL 的函数,并进行一些小技巧来处理 DLL 异常。
object lib::executeFunction(const std::wstring& name, const object::list& paramList) const
{
// Turn the parameter list in JSON
const std::wstring input_json = objectToJson(paramList);
// Call the DLL function and get a JSON response back
wchar_t* output_json_str = m_executer(name.c_str(), input_json.c_str());
if (output_json_str == nullptr)
raiseWError(L"Executing function failed: " + m_filePath + L" - " + name);
// Capture the JSON locally, the call the DLL to free the buffer it allocated
std::wstring output_json = output_json_str;
m_freer(output_json_str);
output_json_str = nullptr;
// Turn the output JSON into an object
object output_obj = objectFromJson(output_json);
// A little hack for detecting exceptions
// Look for an unlikely string prefix, then chop it off to yield a useful error function
if (output_obj.type() == object::STRING)
{
static const wchar_t* expPrefix = L"mscript EXCEPTION ~~~ mscript_ExecuteFunction: ";
static size_t prefixLen = wcslen(expPrefix);
if (startsWith(output_obj.stringVal(), expPrefix))
raiseWError(L"Executing function failed: " + m_filePath + L" - " +
output_obj.stringVal().substr(prefixLen));
}
// All done
return output_obj;
}
结论和关注点
我希望您渴望第一次尝试 mscript,或者如果您以前尝试过,能再给它一次机会。
我期待听到大家对新的错误处理是否易于理解的反馈。
我绝对想听听对 DLL 开发感兴趣的人的反馈。
尽情享用!
历史
- 2022 年 3 月 23 日:初始版本