65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2022 年 3 月 24 日

Apache

5分钟阅读

viewsIcon

10161

downloadIcon

167

如果您还在犹豫是否将 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
}
  1. mscript 调用 verifyLow(13) 函数。
  2. verifyLow() 发现 13 >= 10。
  3. verifyLow() 使用字符串调用 error()。
  4. mscript 在 error() 调用下方查找 "!" 语句。
  5. 在 verifyLow() 中未找到 "!" 语句,错误会冒泡到调用代码。
  6. mscript 在 verifyLow() 调用之后扫描 "!" 语句。
  7. 找到 "!" 语句后,它会执行该 "!" 语句,并将 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 日:初始版本
© . All rights reserved.