生成部分模拟 C++ 类的 C 代码






4.95/5 (4投票s)
C 代码生成器使用程序 make_cpp_class.py 输入文件
介绍
程序 make_c_struct.py 的目的是简化生成 C 头文件和实现文件以模拟 C++ 类某些方面的过程。该程序通常会生成正确的代码,但是,程序无法总是识别要包含的正确头文件,因此生成的代码在成功编译之前可能需要进行编辑。这个程序节省了大量的输入工作。
该程序是在 make_cpp_class 程序编写之后编写的,因为在特定平台上我无法使用 C++。我希望能够使用与 make_cpp_class.py 程序相同的输入文件,但生成 C 代码。该程序旨在接受与 make_cpp_class.py 程序相同的输入文件格式。
C++ 支持封装、继承和多态。该程序模拟 C++ 封装,对于 C++ 虚函数,将模拟多态的代码放入生成的 C 代码中。一些 C++ 特性被忽略了。
该程序适用于经验丰富的 C 程序员,因为理解对生成代码必须进行的更改需要理解 C 语言。如果在输入文件中使用 C++ 特性,也需要理解 C++ 语言。
程序将文件作为输入,以及一个结构名称和一些可选的开关参数。输入文件包含用一种非常简单的语言编写的代码。一旦使用此程序生成代码,输入文件就可以丢弃。
输入文件中任何方法的函数体都会原样复制到生成的代码中。换句话说,您仍然必须编写执行任何操作的代码,该程序只是消除了编写 C 样板代码的需要。
该程序可以运行,但处理虚方法的功能是最近才添加的,尚未经过测试。请定期查看,如果出现任何问题,我将更新代码和历史记录。除了这一个功能之外,该程序已经过测试,并按以下说明运行。
将创建一个与命令行上传递的类名同名的文件夹,并在该文件夹中创建生成的代码。
程序文件
- make_c_struct.py - 解析参数并调用代码生成器的主程序文件。
- bstruct.py - 解析输入文件并创建生成文件的主代码生成模块。
- cparse.py - 解析输入文件。
- file_buffer.py - 缓冲输入文件数据;由 cparse.py 使用
- cfunction_info.py - 存储方法信息,包括方法名、返回类型和参数类型。
- include_file_manager.py - 存储某些标准类型的头文件名。
- ctype_and_name_info.py - 存储一个名称(变量或方法)和一个 type_info 实例。
- ctype_info.py - 存储类型的属性。
- test_input.txt - 一个可以作为程序输入的文件,用于演示某些功能。此文件不属于程序。
使用代码
该程序接受输入文件名、结构名称和一些可选的开关参数作为输入。输入文件包含一种非常简单的语言编写的代码。一旦使用该程序生成代码,输入文件就可以丢弃。
输入文件中任何方法的函数体都会原封不动地复制到生成的代码中。换句话说,您仍然必须编写执行任何操作的代码,该程序只是消除了在生成 C++ 类时通常需要编写的样板代码。
虽然输入语言在下面描述,但查看文件 'test_input.txt' 将使下面的描述更加清晰。
该程序对输入文件中的数据进行相对较少的词法分析。虽然某些序列会被识别为错误,但如果输入文件不正确,程序可能会产生垃圾。这是使用此程序时了解 C++ 的另一个重要原因。
使用代码
该程序接受输入文件名、结构名称和一些可选的开关参数作为输入。输入文件包含一种非常简单的语言编写的代码。一旦使用该程序生成代码,输入文件就可以丢弃。
输入文件中任何方法的函数体都会原封不动地复制到生成的代码中。换句话说,您仍然必须编写执行任何操作的代码,该程序只是消除了在生成 C++ 类时通常需要编写的样板代码。
虽然输入语言在下面描述,但查看文件 'test_input.txt' 将使下面的描述更加清晰。
该程序对输入文件中的数据进行相对较少的词法分析。虽然某些序列会被识别为错误,但如果输入文件不正确,程序可能会产生垃圾。这是使用此程序时了解 C++ 的另一个重要原因。
用法
python make_c_struct.py <struct_name> <input_file_name> [-a author_name] [-f] [-c]
该程序接受以下开关:
-a author_name, --author author_name - 作者姓名。
-f, --full - 写入完整的详细头信息。
-c, --c99comment - 写入 C99 风格的注释。
-h, --help - 显示帮助并退出。
输入文件格式
输入文件中的结构元素格式
从这里开始,结构元素被称为“数据成员”。
数据成员首先出现在文件中,并使用以下格式声明:
<Type> <Name> [= initial_value]<;>
类型可以是指针参数、引用参数,并且可以前面加上 const、volatile 或 static。'Const volatile' 虽然有效,但不允许同时使用。我没有实现这一点只是为了简化解析器;如果需要,请使用一个关键字,并将另一个添加到生成的代码中。一个示例数据成员集可能是:
short m_age;
int m_count = 7;
Foo_t * m_foo;
static const float m_height = 1.0;
如果为数据成员提供了初始值,则等号和终止分号之间的所有内容都用作生成代码中的初始值。虽然 C 中的“static”关键字表示变量具有文件作用域,但输入文件中的 static 关键字表示该变量将被声明为全局变量。
构造函数和析构函数
构造函数被转换为具有以下签名的 C 工厂函数:
struct_name * create<struct_name><digit_string>([argument_list])
第一个构造函数方法的名称中 digit_string 为空。之后,digit_string 为数字字符串“1”,并且每生成一个工厂函数,该值就增加 1。
输入文件中的析构函数生成具有以下签名的函数:
void destroy<struct_name>(<struct_name> * <lower_case_struct_name>_ptr)
在数据成员声明之后,构造函数、析构函数和方法列在输入文件中。所有方法的头文件都会自动写入。方法的返回值可以像数据成员类型一样声明,并且还允许额外的关键字 'inline' 和 'virtual'。这些不能一起使用,并且两者都应该放在函数声明行的开头。
因为类名是在命令行上指定的,所以在用于构造函数或析构函数时,类名在输入文件中使用字符“@”指定。
以下是一些示例,展示了两个构造函数和一个虚析构函数。
@()
{
}
@(int x)
{
// Some code here.
}
virtual ~@()
{
}
为每个构造函数创建成员初始化列表。成员初始化列表可能需要编辑,但很多时候可以保持原样。如果命令行中传递的类名为 'Foobar',并且数据成员列表是上面显示的示例,那么上面显示的以 '@' 字符开头的第一个构造函数定义将生成以下代码:
Foobar::Foobar()
: m_age(0),
, m_count(7)
, m_foo(NULL)
{
}
复制构造函数
在输入行上放置关键字“copy:”将自动生成一个函数代码,该函数对传递的结构进行浅拷贝。
copy:
创建的函数具有以下签名:
<struct_name> * createFoobarCopy(const <struct_name> * source_<lower_case_struct_name>_ptr)
关键字 "nocopy:" 被忽略。方法
所有未声明为“static”的方法都将自动添加一个指向类型为
方法声明类似于 C 函数,但可以通过在末尾放置“const”关键字使方法变为常量。方法后面的“= 0”将被忽略。以下是可以在输入文件中指定的三个方法示例:
如果输入文件中存在任何虚方法,则会创建一个静态函数表,类似于 C++ 的虚函数表。另一种方法是在结构体中包含多个函数指针,每个虚函数一个,但是,如果实例化了许多结构体实例,这将浪费内存。折衷方案是在虚函数调用中增加一个间接层。代码的形式使得很容易改回在结构体中使用多个函数指针。
double accumulate(double addend)
{
m_sum += addend;
return m_sum;
}
const & Foobar GetFoobar() const
{
return m_foobar;
}
virtual int doSomething(int anIntegerToUseForSomething) const
{
// Need to return something here, but make_c_struct.py won't detect
// that no value is returned as the function body is merely copied
// into the method in the generated code.
}
属性方法
还有一个特殊的关键字用于定义属性。属性的数据成员不应在上面提到的数据成员部分声明。
用于生成属性(使用“:property”关键字)的语法是:
:property <type> <data_member_name> <property_name>
此定义将创建一个名为 set
以下是一个示例属性定义。
property: int m_age Age
上述属性定义将导致代码生成器编写以下数据成员(结构元素)和方法。为简洁起见,此处省略了代码头。此外,数据成员 m_age 将在类的头文件的数据成员部分中声明。
int m_age;
int Age() const
{
return m_age;
}
void setAge(int the_value)
{
m_age = the_value;
}
如果在属性声明中使用的数据类型不是固有类型,则代码生成器将生成与上面所示略有不同的代码。以下是一个示例:property: Person_t * m_person Person
生成的代码是Person_t * m_person;
Person_t Person() const
{
return m_person;
}
void setPerson(const Person_t * the_value)
{
m_person = the_value;
}
消息正文
解析器通过解析方法签名到第一个左大括号字符“{”来检测方法体的开始。
此时,忽略字符串声明中包含的括号,一个左大括号会导致一个初始为零的计数器递增,而一个右大括号“}”会导致计数器递减。当计数返回零时,方法体结束。
如果输入文件中的括号不正确,程序将生成不正确的代码。
这个程序会犯的错误
如前所述,输入文件的词法分析最少,因此输入垃圾将导致输出垃圾。
此外,程序假设所有非固有(非内置)类型或非包含文件管理器代码中特殊类型名称的类型名称,都将生成一个“include”语句,以包含一个与类型名称相同且扩展名为“.h”的头文件。当然,这通常不是一个有效的头文件名,因此通常需要删除“include”语句或更改“include”语句中头文件的名称。
此外,代码主体中的任何数据类型都不会被检测到,并且可能需要为这些类型在生成的代码中包含头文件。
提示与技巧
如果我有一些我知道程序不会生成的数据,例如注释块或“include 语句”,并且我希望这些数据存在于类头文件中,我将声明一个内联函数,例如:
inline void dummy()
{
#include "foobar.h"
#include "barfoo.h"
}
稍后,我编辑文件,将文本移动到它所属的位置,并删除不需要的方法。此外,使用“@”作为类名允许通过多次运行程序,传入不同的类名和相同的基类作为输入,从而生成具有相同方法集的多个类,所有这些类都派生自同一个基类。
兴趣点
该程序的编写源于希望消除编写 C++ 头文件和 C++ 实现文件中所有冗余信息的必要性,并编写每个类所需的所有样板代码。
这个程序可以做得更好,但如果想编写一个能处理所有情况且没有错误的合适程序,可能需要数月甚至数年。对我来说,这比仅仅编写这个工具然后修复包含文件中的小错误要慢。
尽管如此,如果成千上万的人要生成代码,那么创建一个输入语言语法,并使用 yacc 或 ANTLR 来创建解析器,然后再添加一个代码生成器可能是有益的。这个假设的程序可以处理消息体的解析,并且会更加健壮。
历史
初次发布