.. _embedding_modules: ======================== 模块和 C++ 绑定 ======================== --------------- 内置模块 --------------- 内置模块是向 Daslang 公开 C++ 功能的方法。 让我们以 ``FIO`` 模块为例。 要创建内置模块,应用程序需要执行以下作: 在模块所在的位置创建一个 C++ 文件。此外,为 AOT 创建一个标头以包含。 从 Module 类派生,并向构造函数提供自定义模块名称:: class Module_FIO : public Module { public: Module_FIO() : Module("fio") { // 此模块名称为 ``FIO`` DAS_PROFILE_SECTION("Module_FIO"); // profile 部分用于分析模块初始化时间 ModuleLibrary lib; // module 需要 library 来注册类型和函数 lib.addModule(this); // 将当前模块添加到库中 lib.addBuiltInModule(); // 使内置函数对库可见 指定 AOT 类型并提供带有 C++ includes 的前缀 (参阅 :ref:`AOT `):: virtual ModuleAotType aotRequire ( TextWriter & tw ) const override { tw << "#include \"Daslang/simulate/aot_builtin_fio.h\"\n"; return ModuleAotType::cpp; } 使用 ``REGISTER_MODULE`` 或 ``REGISTER_MODULE_IN_NAMESPACE`` 宏在 C++ 文件的底部注册模块:: REGISTER_MODULE_IN_NAMESPACE(Module_FIO,das); 在调用 Daslang 编译器之前,在应用程序初始化期间使用 ``NEED_MODULE`` 宏:: NEED_MODULE(Module_FIO); 内置模块可以附带额外的 Daslang 文件,并在初始化时通过 ``compileBuiltinModule`` 函数编译它们:: Module_FIO() : Module("fio") { ... // 添加内置模块 compileBuiltinModule("fio.das",fio_das, sizeof(fio_das)); 这里发生的情况是 fio.das 作为字符串常量嵌入到可执行文件中(通过 XXD 实用程序)。 一旦所有内容都注册到模块类构造函数中,最好通过 ``verifyAotReady`` 函数验证模块是否已准备好进行 AOT。 验证内置名称是否遵循正确的命名约定并且不会通过 ``verifyBuiltinNames`` 函数与关键字冲突也是一个好主意:: Module_FIO() : Module("fio") { ... // 让我们验证所有名字 uint32_t verifyFlags = uint32_t(VerifyBuiltinFlags::verifyAll); verifyFlags &= ~VerifyBuiltinFlags::verifyHandleTypes; // 我们跳过由于 FILE 和 FStat 而引起的 annotatins verifyBuiltinNames(verifyFlags); // 现在它准备好了 verifyAotReady(); } ------------- ModuleAotType ------------- 模块可以指定 3 个不同的 AOT 选项。 ``ModuleAotType::no_aot`` 意味着不会对该模块以及需要它的任何其他模块进行提前编译。 ``ModuleAotType::hybrid`` 意味着不会对 Module 本身进行提前编译。 其他需要此模块的模块将具有 AOT,但并非没有性能损失。 ``ModuleAotType::cpp`` 意味着将发生全面的 AOT。 这也意味着模块需要为每个函数、字段或属性填写 ``cppName``。 验证它的最佳方法是在模块构造函数的末尾调用 ``verifyAotReady`` 。 此外,模块需要写出所需的 C++ 包含的完整列表:: virtual ModuleAotType aotRequire ( TextWriter & tw ) const override { tw << "#include \"Daslang/simulate/aot_builtin_fio.h\"\n"; // like this return ModuleAotType::cpp; } ------------------------ 内置模块常量 ------------------------ 常量可以通过 ``addConstant`` 函数来暴露:: addConstant(*this,"PI",(float)M_PI); 常量的类型是自动推断的,假设 type ``cast`` 基础设施就位(参阅 :ref:`cast `). --------------------------- 内置模块枚举 --------------------------- 枚举可以通过 `the `addEnumeration`` 函数公开:: addEnumeration(make_smart()); addEnumeration(make_smart()); 为此,必须通过 ``DAS_BASE_BIND_ENUM`` 或 ``DAS_BASE_BIND_ENUM_98`` C++ 预处理器宏来定义枚举适配器:: namespace Goo { enum class GooEnum { regular , hazardous }; enum GooEnum98 { soft , hard }; } DAS_BASE_BIND_ENUM(Goo::GooEnum, GooEnum, regular, hazardous) DAS_BASE_BIND_ENUM_98(Goo::GooEnum98, GooEnum98, soft, hard) ------------------------- 内置模块数据类型 ------------------------- 自定义数据类型和类型注释可以通过 ``addAnnotation`` 或 ``addStructure`` 函数来公开:: addAnnotation(make_smart(lib)); 见 :ref:`handles ` 了解更多细节。 ------------------------- 内置模块宏 ------------------------- 不同类型的自定义宏可以通过 ``addAnnotation``, ``addTypeInfoMacro``, ``addReaderMacro``, ``addCallMacro`` 等方式添加。 但是,强烈建议在 Daslang 中实现宏。 见 :ref:`macros ` 了解更多细节。 ------------------------ 内置模块函数 ------------------------ 函数可以通过 ``addExtern`` 和 ``addInterop`` 例程暴露给内置模块。 ~~~~~~~~~ addExtern ~~~~~~~~~ ``addExtern`` 公开不是专门为 Daslang 互作设计的标准 C++ 函数:: addExtern(*this, lib, "fprint", SideEffects::modifyExternal, "builtin_fprint"); 在这里,builtin_fprint 函数向 Daslang 公开,并被命名为 `fprint`。 显式指定函数的 AOT 名称,以指示该函数已准备好 AOT。 需要显式指定函数的副作用 (参阅 :ref:`Side-effects `). 指定 ``SideEffects::worstDefault`` 总是安全的,但效率低下。 我们来详细看看暴露的函数:: void builtin_fprint ( const FILE * f, const char * text, Context * context, LineInfoArg * at ) { if ( !f ) context->throw_error_at(at, "can't fprint NULL"); if ( text ) fputs(text,(FILE *)f); } C++ 代码可以通过添加 `Context` 类型参数来显式请求提供 Daslang 上下文。 将其设为函数的最后一个参数使上下文替换对 Daslang 代码透明,即它可以简单地调用:: fprint(f, "boo") // 以透明方式提供当前上下文 Daslang 字符串与 C++ ``char *``非常相似,但 null 也表示空字符串。 这就是为什么在上面的示例中,只有当 text 不为 null 时才会出现 `fputs` 的原因。 让我们看看内置`math`模块中的另一个集成示例:: addExtern(*this, lib, "translation", SideEffects::none, "float4x4_translation")->arg("xyz"); 在这里,float4x4_translation 函数按值返回 ref 类型,即 `float4x4`. 这需要通过为 ``addExtern`` 函数指定一个模板化的 SimNode 参数来明确表示,即 ``SimNode_ExtFuncCallAndCopyOrMove``。 一些函数需要通过引用返回 ref 类型:: addExtern(*this, lib, "fooPtr2Ref", SideEffects::none, "fooPtr2Ref"); 这由 ``SimNode_ExtFuncCallRef`` 参数表示。 ~~~~~~~~~~ addInterop ~~~~~~~~~~ 对于某些函数,可能需要访问类型信息以及非封送数据。 互作函数是专门为此目的而设计的。 互作函数具有以下模式:: vec4f your_function_name_here ( Context & context, SimNode_CallBase * call, vec4f * args ) 它们接收上下文、调用节点和参数。 它们需要封送并返回结果,即 v_zero()。 ``addInterop`` 公开 C++ 函数,这些函数是专门围绕 Daslang 设计的:: addInterop< builtin_read, // 注册函数 int, // 函数返回类型 const FILE*,vec4f,int32_t // 按顺序排列的函数参数 >(*this, lib, "_builtin_read",SideEffects::modifyExternal, "builtin_read"); 互作函数注册模板需要函数名称作为其第一个模板参数,函数返回值作为其第二个参数,其余参数紧随其后。 当函数的参数类型需要保持未指定状态时,使用 ``vec4f`` 的参数类型。 我们来详细看看暴露的函数:: vec4f builtin_read ( Context & context, SimNode_CallBase * call, vec4f * args ) { DAS_ASSERT ( call->types[1]->isRef() || call->types[1]->isRefType() || call->types[1]->type==Type::tString); auto fp = cast::to(args[0]); if ( !fp ) context.throw_error_at(call->debugInfo, "can't read NULL"); auto buf = cast::to(args[1]); auto len = cast::to(args[2]); int32_t res = (int32_t) fread(buf,1,len,fp); return cast::from(res); } 可以通过 call->types 数组访问参数类型。 参数值和返回值通过 ``cast`` 基础设施封送 (参阅 :ref:`cast `). .. _modules_function_sideeffects: --------------------- 函数副作用 --------------------- Daslang 编译器在很大程度上是一个 optimizin 编译器,并且非常关注函数的副作用。 在 C++ 方面, ``enum class SideEffects`` 包含可能的副作用组合。 ``none`` 表示函数是纯函数,即它没有任何副作用。 一个很好的例子是纯计算函数,如 ``cos`` 或 ``strlen``. Daslang 可以选择在编译时折叠这些函数,也可以在不使用结果的情况下完全删除它们。 尝试注册没有参数且没有副作用的 void 函数会导致模块初始化失败。 ``unsafe`` 表示函数具有不安全的副作用,这可能会导致 panic 或 crash。 ``userScenario`` 表示其他一些未分类的副作用正在发生。 Daslang 不会优化或折叠这些功能。 ``modifyExternal`` 指示该函数修改 state 外部 Daslang; 通常是某种 C++ 状态。 ``accessExternal`` 指示该函数读取 state 外部 Daslang。 ``modifyArgument`` 表示该函数修改其输入参数之一。 Daslang 将研究非常量 ref 参数,并假设它们在函数调用期间可能会被修改。 尝试注册没有可变 ref 参数和 ``modifyArgument`` 副作用的函数会导致模块初始化失败。 ``accessGlobal`` 表示该函数访问全局状态,即全局 Daslang 变量或常量。 ``invoke`` 表示该函数可以调用其他函数、lambda 或块。 .. _modules_file_access: ----------- 文件访问 ----------- Daslang 提供了指定自定义文件访问和模块名称解析的机制。 默认文件访问是通过 ``FsFileAccess`` 类实现的。 文件访问需要实现以下文件和名称解析例程:: virtual das::FileInfo * getNewFileInfo(const das::string & fileName) override; virtual ModuleInfo getModuleInfo ( const string & req, const string & from ) const override; ``getNewFileInfo`` 提供文件数据机械的文件名。如果未找到文件,则返回 null。 ``getModuleInfo`` 为文件名解析机制提供模块名称。 给定 require string `req` 和它被称为 `from` 的模块,它需要完全解析该模块:: struct ModuleInfo { string moduleName; // 模块的名称(默认情况下为 req 的尾部) string fileName; // 文件名,模块所在的位置 string importName; // import name,即 module namespace (默认与 module name 相同) }; 最好通过项目在 Daslang 本身中实现模块解析。 .. _modules_project: ------- Project ------- 项目需要导出一个 ``module_get`` 函数,它基本上实现了默认的 C++ ``getModuleInfo`` 例程:: require strings require daslib/strings_boost typedef module_info = tuple const // mirror of C++ ModuleInfo [export] def module_get(req,from:string) : module_info let rs <- split_by_chars(req,"./") // split request var fr <- split_by_chars(from,"/") let mod_name = rs[length(rs)-1] if length(fr)==0 // relative to local return [[auto mod_name, req + ".das", ""]] elif length(fr)==1 && fr[0]=="daslib" // process `daslib` prefix return [[auto mod_name, "{get_das_root()}/daslib/{req}.das", ""]] else pop(fr) for se in rs push(fr,se) let path_name = join(fr,"/") + ".das" // treat as local path return [[auto mod_name, path_name, ""]] 上面的实现拆分了 require 字符串并查找已识别的前缀。 如果从另一个模块请求模块,则使用父模块前缀。 如果识别出根 `daslib` 前缀,则从 ``get_das_root`` 路径中查找模块。 否则,请求将被视为本地路径。