1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright (c) 2022 Shenzhen Kaihong Digital Industry Development Co., Ltd.
5#
6# HDF is dual licensed: you can use it either under the terms of
7# the GPL, or the BSD license, at your option.
8# See the LICENSE file in the root of this repository for complete details.
9
10import argparse
11import os
12import re
13import stat
14
15import pip
16
17
18def lost_module(module_name):
19    print("""
20need %s module, try install first:
21
22    pip install %s""" % (module_name, module_name))
23    exit()
24
25
26class IDLGenerator:
27    def __init__(self):
28        self._idl = []
29        self._output_path = ""
30        self._file_list = []
31        self._key_list = {}
32        self._typedef_list = {}
33        self._parse_results = {}
34
35    def generate(self):
36        try:
37            self._parse_option()
38            self._parse_header()
39            for i in self._parse_results:
40                self._generate_type(self._parse_results[i])
41                self._generate_interface(self._parse_results[i])
42        except KeyError:
43            pass
44        finally:
45            pass
46
47    def _parse_option(self):
48        parser = argparse.ArgumentParser(description="Compile C/C++ header files and generate .idl files.")
49        parser.add_argument("-v", "--version", help="Display version information", action="store_true")
50        parser.add_argument("-f", "--file", metavar="<*.h>", required=True, action='append',
51                            help="Compile the C/C++ header file")
52        parser.add_argument("-o", "--out", metavar="<directory>", default=".", required=True,
53                            help="Place generated .idl files into the <directory>")
54        args = parser.parse_args()
55
56        self._file_list = args.file
57        self._output_path = args.out
58
59    def _search_file(self, root_path, file_name):
60        file_list = os.listdir(root_path)
61        for file in file_list:
62            path = os.path.join(root_path, file)
63            if os.path.isdir(path):
64                file_path = self._search_file(path, file_name)
65                if file_path is not None:
66                    return file_path
67            if os.path.isfile(path):
68                if os.path.split(path)[1] == file_name:
69                    return path
70
71    def _parse_include_file(self, file_list, root_path):
72        include_file = []
73        for header_file in file_list:
74            result = HeaderParser().parse(header_file)
75            if result is None:
76                continue
77            self._parse_results[result.get("name")] = result
78            for file_name in result.get("import", None):  # 把include的文件加入列表
79                if file_name in self._parse_results and file_name is not None:  # 解析过的不重复解析
80                    continue
81                file_path = self._search_file(root_path, file_name)
82                if file_path is not None:
83                    include_file.append(file_path)
84        return include_file
85
86    def _parse_header(self):
87        try:
88            for file in self._file_list:
89                root_path, _ = os.path.split(file)  # 输入文件所在目录
90                file_list = [file]  # 需要解析的头文件列表,包括include包含的头文件
91                while len(file_list) > 0:
92                    file_list = self._parse_include_file(file_list, root_path)
93
94            for i in self._parse_results:
95                for enum in self._parse_results[i]["enum"]:
96                    self._key_list[enum["name"]] = "enum"
97                for union in self._parse_results[i]["union"]:
98                    self._key_list[union["name"]] = "union"
99                for struct in self._parse_results[i]["struct"]:
100                    self._key_list[struct["name"]] = "struct"
101                for clas in self._parse_results[i]["interface"]:
102                    self._key_list[clas["name"]] = ""
103                for cb in self._parse_results[i]["callback"]:
104                    self._key_list[cb["name"]] = ""
105                for td in self._parse_results[i]["typedef"]:
106                    self._typedef_list[td["name"]] = td["type"]
107        except KeyError:
108            pass
109        finally:
110            pass
111
112    def _generate_type(self, header):
113        if self._has_user_define_type(header):
114            self._install_package(header["path"])
115            self._install_import(header)
116            self._install_enum(header["enum"])
117            self._install_stack(header["union"])
118            self._install_stack(header["struct"])
119
120            self._write_file(header["path"], "Types.idl")
121
122    def _generate_interface(self, header):
123        for iface in header["interface"]:
124            self._install_package(header["path"])
125            self._install_import(header)
126            self._install_interface(iface)
127
128            self._write_file(header["path"], iface["name"] + ".idl")
129
130    def _install_package(self, file_path):
131        self._idl += "package " + self._get_package(file_path) + ";\n\n"
132
133    def _install_import(self, header):
134        try:
135            original_idl = self._idl
136            for file_name in header["import"]:
137                if file_name not in self._parse_results:
138                    self._idl.append("// can't import %s\n" % file_name)
139                    print("[IDLGenerator]: %s[line ?] can't find %s"
140                          % (os.path.normpath(header["path"] + "/" + header["name"]), file_name))
141                    continue
142                include_file = self._parse_results[file_name]
143                if self._has_user_define_type(include_file):
144                    tt = re.search("import\\s+\\S+.Types", "".join(self._idl))
145                    if tt is None:
146                        self._idl.append("import %s.Types;\n" % self._get_package(include_file['path']))
147                for iface in include_file["interface"]:
148                    self._idl.append("import %s.%s;\n" % (self._get_package(include_file['path']), iface["name"]))
149                    # self._idl +=
150                for cb in include_file["callback"]:
151                    self._idl.append("import %s.%s;\n" % (self._get_package(include_file['path']), cb["name"]))
152            for cb in header['callback']:
153                self._idl.append("import %s.%s;\n" % (self._get_package(header['path']), cb["name"]))
154
155            if original_idl != "".join(self._idl):
156                self._idl.append("\n")
157        except KeyError:
158            pass
159        finally:
160            pass
161
162    def _install_enum(self, enums):
163        for enum in enums:
164            self._idl.append("enum %s {\n" % enum["name"])
165            for member in enum["members"]:
166                self._idl.append("    %s = %s,\n" % (member["name"], member["value"]))
167            self._idl.append("};\n")
168
169    def _install_stack(self, stacks):
170        for stack in stacks:
171            self._idl.append(stack["type"] + " %s {\n" % stack["name"])
172            for member in stack["members"]:
173                param_type = self._swap_type_c2idl(member["type"])
174                if "unknown type" in param_type:
175                    print("[IDLGenerator]: %s[line %d] %s" % (
176                        os.path.normpath(member["file_name"]), member["line_number"], param_type))
177                self._idl.append("    %s %s;\n" % (param_type, member["name"]))
178            self._idl.append("};\n")
179
180    def _install_interface(self, iface):
181        if re.search("[Cc]all[Bb]ack", iface["name"]):
182            self._idl.append("[callback] ")
183        self._idl.append("interface %s {\n" % iface["name"])
184        for member in iface["members"]:
185            self._install_function(iface["name"], member)
186        self._idl.append("}\n")
187
188    def _install_function(self, iface_name, member):
189        self._idl.append("    %s(" % member["name"])
190        for i, param in enumerate(member["params"]):
191            tt = re.fullmatch(r"(enum)*(union)*(struct)* *%s *\** * *\**" % iface_name, param["type"])
192            if tt:
193                continue
194            param_type = self._swap_type_c2idl(param["type"])
195            if "unknown type" in param_type:
196                print("[IDLGenerator]: %s[line %d] %s" % (
197                    os.path.normpath(member["file_name"]), member["line_number"], param_type))
198            self._idl.append("%s %s %s," % (
199                '* *' in param["type"] and "[out]" or "[in]",
200                param_type,
201                param["name"]))
202        if "".join(self._idl).endswith(','):
203            self._idl = ["".join(self._idl)[:-1], ");\n"]
204        else:
205            self._idl.append(");\n")
206
207    @staticmethod
208    def _convert_basic_type(c_type):
209        type_c2idl = [
210            [["bool"], "boolean"],
211            [["int8_t", "char"], "byte"],
212            [["int16_t"], "short"],
213            [["int32_t", "int"], "int"],
214            [["int64_t"], "long"],
215            [["float"], "float"],
216            [["double"], "double"],
217            [["std::string", "string", "cstring"], "String"],
218            [["uint8_t", "unsigned char"], "unsigned char"],
219            [["uint16_t", "unsigned short"], "unsigned short"],
220            [["uint32_t", "unsigned int"], "unsigned int"],
221            [["uint64_t", "unsigned long"], "unsigned long"],
222            [["void"], "void"]
223        ]
224        for cti in type_c2idl:
225            for ct in cti[0]:
226                tt = re.match(r"(const )* *%s *(\**) *" % ct, c_type)
227                if not tt:
228                    continue
229                idl_type = cti[1]
230                if c_type.count('*') == 1:
231                    idl_type += '[]'
232                return idl_type
233        return ""
234
235    def _convert_structure(self, c_type):
236        for type_name in self._key_list:
237            if "_ENUM_POINTER" in c_type:
238                c_type = c_type.replace("_ENUM_POINTER", " * ")
239            tt = re.fullmatch(r"(const )* *(enum)*(union)*(struct)* *%s *[*&]* * *[*&]*" % type_name, c_type)
240            if tt:
241                if len(self._key_list.get(type_name)) > 0:
242                    idl_type = self._key_list.get(type_name) + ' ' + type_name
243                else:
244                    idl_type = type_name
245                if c_type.count('*') == 1:
246                    idl_type += '[]'
247                return idl_type
248        return ""
249
250    def _convert_typedef(self, c_type):
251        for type_name in self._typedef_list:
252            tt = re.match(r"(const )* *%s *\** *" % type_name, c_type)
253            if tt:
254                if self._typedef_list.get(type_name).count('*') == 1:
255                    idl_type = self._typedef_list.get(type_name).split(' ')[0] + '[]'
256                else:
257                    idl_type = self._typedef_list.get(type_name)
258                return idl_type
259        return ""
260
261    def _convert_container_type(self, c_type):
262        type_pattern = " *(std::)*(struct *)*((unsigned *)*\\w+) *"
263        tt = re.match("(const *)* *(std::)*map *<%s, %s>" % (type_pattern, type_pattern), c_type)
264        if tt:
265            key_type = self._convert_basic_type(tt[5])
266            value_type = self._convert_basic_type(tt[9])
267            return "Map<%s, %s>" % (key_type == "" and tt[5] or key_type, value_type == "" and tt[9] or value_type)
268
269        tt = re.match("(const *)* *(std::)*vector *<%s>" % type_pattern, c_type)
270        if tt:
271            v_type = self._convert_basic_type(tt[5])
272            return "List<%s>" % (v_type == "" and tt[5] or v_type)
273        return ""
274
275    def _swap_type_c2idl(self, c_type):
276        idl_type = self._convert_basic_type(c_type)
277        if idl_type != "":
278            return idl_type
279
280        idl_type = self._convert_structure(c_type)
281        if idl_type != "":
282            return idl_type
283
284        idl_type = self._convert_typedef(c_type)
285        if idl_type != "":
286            return idl_type
287
288        idl_type = self._convert_container_type(c_type)
289        if idl_type != "":
290            return idl_type
291
292        idl_type = "/* unknown type: [%s] */" % c_type
293        return idl_type
294
295    def _write_file(self, file_path, file_name):
296        file = os.path.join(self._make_output_dir(file_path), file_name).replace('\\', '/')
297        flags = os.O_RDWR | os.O_CREAT
298        modes = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH
299        with os.fdopen(os.open(file, flags, modes), "w", encoding="utf-8") as fp:
300            fp.write("".join(self._idl))
301        print("Generate: --------------------- %s ---------------------\n" % os.path.normpath(file))
302        self._idl = []
303
304    @staticmethod
305    def _split_path(file_path):
306        normpath = os.path.normpath(file_path)
307        drive, out_path = os.path.splitdrive(normpath)
308        out_path = out_path.replace('\\', '/')
309        new_path = out_path.strip('.').strip('/')
310        while new_path != out_path:
311            out_path = new_path
312            new_path = out_path.strip('.').strip('/')
313
314        return new_path
315
316    def _make_output_dir(self, file_path):
317        output_dir = self._output_path + '/' + self._split_path(file_path)
318        if not os.path.exists(output_dir):
319            os.makedirs(output_dir)
320        return output_dir
321
322    @staticmethod
323    def _has_user_define_type(h):
324        if len(h["enum"]) > 0 or len(h["union"]) > 0 or len(h["struct"]) > 0:
325            return True
326        return False
327
328    def _get_package(self, file_path):
329        path_name = re.split(r'[/\\]', self._split_path(file_path))
330        out_path_temp = []
331        for name in path_name:
332            out_path_temp.append(name)
333        return ".".join(out_path_temp)
334
335
336if __name__ == "__main__":
337    try:
338        import CppHeaderParser
339        from _header_parser import HeaderParser
340    except ImportError:
341        pip.main(["--disable-pip-version-check", "install", "robotpy-cppheaderparser"])
342        try:
343            import CppHeaderParser
344            from _header_parser import HeaderParser
345        except ImportError:
346            HeaderParser = None
347            lost_module("robotpy-cppheaderparser")
348        finally:
349            pass
350    finally:
351        pass
352    generator = IDLGenerator()
353    generator.generate()