3.2. 模块和 C++ 绑定
3.2.1. 内置模块
内置模块是向 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 的前缀 (参阅 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();
}
3.2.2. 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;
}
3.2.3. 内置模块常量
常量可以通过 addConstant
函数来暴露:
addConstant(*this,"PI",(float)M_PI);
常量的类型是自动推断的,假设 type cast
基础设施就位(参阅 cast).
3.2.4. 内置模块枚举
枚举可以通过 the `addEnumeration` 函数公开:
addEnumeration(make_smart<EnumerationGooEnum>());
addEnumeration(make_smart<EnumerationGooEnum98>());
为此,必须通过 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)
3.2.5. 内置模块数据类型
自定义数据类型和类型注释可以通过 addAnnotation
或 addStructure
函数来公开:
addAnnotation(make_smart<FileAnnotation>(lib));
见 handles 了解更多细节。
3.2.6. 内置模块宏
不同类型的自定义宏可以通过 addAnnotation
, addTypeInfoMacro
, addReaderMacro
, addCallMacro
等方式添加。
但是,强烈建议在 Daslang 中实现宏。
见 macros 了解更多细节。
3.2.7. 内置模块函数
函数可以通过 addExtern
和 addInterop
例程暴露给内置模块。
3.2.7.1. addExtern
addExtern
公开不是专门为 Daslang 互作设计的标准 C++ 函数:
addExtern<DAS_BIND_FUN(builtin_fprint)>(*this, lib, "fprint", SideEffects::modifyExternal, "builtin_fprint");
在这里,builtin_fprint 函数向 Daslang 公开,并被命名为 fprint。 显式指定函数的 AOT 名称,以指示该函数已准备好 AOT。
需要显式指定函数的副作用 (参阅 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<DAS_BIND_FUN(float4x4_translation), SimNode_ExtFuncCallAndCopyOrMove>(*this, lib, "translation",
SideEffects::none, "float4x4_translation")->arg("xyz");
在这里,float4x4_translation 函数按值返回 ref 类型,即 float4x4.
这需要通过为 addExtern
函数指定一个模板化的 SimNode 参数来明确表示,即 SimNode_ExtFuncCallAndCopyOrMove
。
一些函数需要通过引用返回 ref 类型:
addExtern<DAS_BIND_FUN(fooPtr2Ref),SimNode_ExtFuncCallRef>(*this, lib, "fooPtr2Ref",
SideEffects::none, "fooPtr2Ref");
这由 SimNode_ExtFuncCallRef
参数表示。
3.2.7.2. 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<FILE *>::to(args[0]);
if ( !fp ) context.throw_error_at(call->debugInfo, "can't read NULL");
auto buf = cast<void *>::to(args[1]);
auto len = cast<int32_t>::to(args[2]);
int32_t res = (int32_t) fread(buf,1,len,fp);
return cast<int32_t>::from(res);
}
可以通过 call->types 数组访问参数类型。
参数值和返回值通过 cast
基础设施封送 (参阅 cast).
3.2.8. 函数副作用
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 或块。
3.2.9. 文件访问
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 本身中实现模块解析。
3.2.10. Project
项目需要导出一个 module_get
函数,它基本上实现了默认的 C++ getModuleInfo
例程:
require strings
require daslib/strings_boost
typedef
module_info = tuple<string;string;string> 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
路径中查找模块。
否则,请求将被视为本地路径。