pymavlink 源码剖析(一)之XML文件的数据解析

 2023-09-19 阅读 17 评论 0

摘要:文章目录1 引言2 pymavlink 的代码自动生成方法3 XML 文件的数据解析3.1 XML 文件预处理3.2 解析 XML 的数据3.2.1 依据协议版本初始化一些版本特征变量3.2.2 解析 XML 文件3.2.3 对解析后结果的后处理4 目标代码生成 相关: MAVLink 协议解析之原理篇;MAVLink 协议解析

文章目录

    • 1 引言
    • 2 pymavlink 的代码自动生成方法
    • 3 XML 文件的数据解析
      • 3.1 XML 文件预处理
      • 3.2 解析 XML 的数据
        • 3.2.1 依据协议版本初始化一些版本特征变量
        • 3.2.2 解析 XML 文件
        • 3.2.3 对解析后结果的后处理
      • 4 目标代码生成

相关:

  1. MAVLink 协议解析之原理篇;
  2. MAVLink 协议解析之XML定义篇
  3. pymavlink 源码剖析(二)之生成代码

1 引言

pymavlink 是 MAVLink 协议的 Python 实现,同时它还是一个 MAVLink 协议代码实现的自动生成工具,目前支持的语言有 CC++11PythonJavaJavascriptTypescriptC#wluaObj-C。 本文是 pymavlink 源码剖析 文章的第一篇,内容可以分为两部分:一是概述了 pymavlink 的实现代码自动生成的基本流程;二是仔细描述了 pymavlink 的XML 文件的数据解析流程,而本篇的题目即以此命名。
如果对 MAVLink 协议还不太熟悉请参考文章目录下方 “相关” 里面给出的链接。

2 pymavlink 的代码自动生成方法

pymavlink 的代码 clone 下来之后的文件目录结构如下
pymavlink 文件目录结构
MAVLink 的官方文档可以知道 Tools/mavgen.py 是pymavlink 代码生成的入口程序。其内容如下

代码段1

#!/usr/bin/env python'''
parse a MAVLink protocol XML file and generate a python implementationCopyright Andrew Tridgell 2011
Released under GNU GPL version 3 or later'''# allow running mavgen from within the tree without installing
if __name__ == "__main__" and __package__ in ('', None):from os import sys, pathsys.path.insert(0, path.dirname(path.dirname(path.dirname(path.abspath(__file__)))))from pymavlink.generator import mavgen
from pymavlink.generator import mavparsefrom argparse import ArgumentParserparser = ArgumentParser(description="This tool generate implementations from MAVLink message definitions")
parser.add_argument("-o", "--output", default="mavlink", help="output directory.")
parser.add_argument("--lang", dest="language", choices=mavgen.supportedLanguages, default=mavgen.DEFAULT_LANGUAGE, help="language of generated code [default: %(default)s]")
parser.add_argument("--wire-protocol", choices=[mavparse.PROTOCOL_0_9, mavparse.PROTOCOL_1_0, mavparse.PROTOCOL_2_0], default=mavgen.DEFAULT_WIRE_PROTOCOL, help="MAVLink protocol version. [default: %(default)s]")
parser.add_argument("--no-validate", action="store_false", dest="validate", default=mavgen.DEFAULT_VALIDATE, help="Do not perform XML validation. Can speed up code generation if XML files are known to be correct.")
parser.add_argument("--error-limit", default=mavgen.DEFAULT_ERROR_LIMIT, help="maximum number of validation errors to display")
parser.add_argument("--strict-units", action="store_true", dest="strict_units", default=mavgen.DEFAULT_STRICT_UNITS, help="Perform validation of units attributes.")
parser.add_argument("definitions", metavar="XML", nargs="+", help="MAVLink definitions")
args = parser.parse_args()mavgen.mavgen(args, args.definitions)

py是什么文件、由其内容可以看出它实际上是对 generator/mavgen.py 的简单调用, 所以这里主要关注的是 generator/mavgen.py 的内容,mavgen.py 所在文件夹 generator 包含了自动生成代码所需的主要文件,其文件夹结构如下:
generator 文件夹结构
可以看到在文件夹下有以 mavgen_ 开头后面跟着编程语言名称的几个 .py 文件、XML结构定义文件mavschema.xsd 、几个以编程语言名称命名的文件夹,以及一些其他文件。 mavgen.py 包含了入口函数 mavgen ,其主要执行以下几个项内容:

  1. XML 文件预处理: 检查输入的XML 文件是否有效
  2. 解析 XML 的数据,同时把<include> 标签引入的XML 文件添加到待处理列表里
  3. 生成指定编程语言的 MAVLink 协议代码

下面对每一部分分别进行介绍。

3 XML 文件的数据解析

3.1 XML 文件预处理

mavgen 这个函数里,定义了内部函数 mavgen_validate 用来完成了上一节中提到的 XML 文件的有效性校验。mavgen_validate 函数的内容如下:

代码段 2

    def mavgen_validate(xmlfile):"""Uses lxml to validate an XML file. We define mavgen_validatehere because it relies on the XML libs that were loaded in mavgen(), so it can't be called standalone"""xmlvalid = Truetry:with open(xmlfile, 'r') as f:xmldocument = etree.parse(f)xmlschema.assertValid(xmldocument)forbidden_names_re = re.compile("^(break$|case$|class$|catch$|const$|continue$|debugger$|default$|delete$|do$|else$|\export$|extends$|finally$|for$|function$|if$|import$|in$|instanceof$|let$|new$|\return$|super$|switch$|this$|throw$|try$|typeof$|var$|void$|while$|with$|yield$|\enum$|await$|implements$|package$|protected$|static$|interface$|private$|public$|\abstract$|boolean$|byte$|char$|double$|final$|float$|goto$|int$|long$|native$|\short$|synchronized$|transient$|volatile$).*", re.IGNORECASE)for element in xmldocument.iter('enum', 'entry', 'message', 'field'):if forbidden_names_re.search(element.get('name')):print("Validation error:", file=sys.stderr)print("Element : %s at line : %s contains forbidden word" % (element.tag, element.sourceline), file=sys.stderr)xmlvalid = Falsereturn xmlvalidexcept etree.XMLSchemaError:return Falseexcept etree.DocumentInvalid as err:sys.exit('ERROR: %s' % str(err.error_log))return True

这段代码主要对XML 的有效性做了三个方面的检验:
(1)是否是有效的XML 格式的文件
(2)是否符合 mavschema.xsd 文件的定义
(3)是否在标签的属性里含有不允许的字符。这里的不允许字符串主要是用来检查是否和目标语言的关键字冲突。

3.2 解析 XML 的数据

解析XML 的调用的是 mavparse.py 中定义的 MAVXML 类,

142  xml.append(mavparse.MAVXML(fname, opts.wire_protocol))

Xml文件?下面分析 MAVXML 类。MAVXML 类的代码框架如下

代码段 3

class MAVXML(object):'''parse a mavlink XML file'''def __init__(self, filename, wire_protocol_version=PROTOCOL_0_9):#initial codedef __str__(self):return "MAVXML for %s from %s (%u message, %u enums)" % (self.basename, self.filename, len(self.message), len(self.enum))

可以看到 MAVXML 类只是重载了__init____str__,并没有更多的方法定义,同时注意到 __str__ 作用是把XML 的一些文件信息给返回。下面主要分析一下类初始化函数 __init____init__ 函数的定义如 代码块 3 中所示。第一个参数是文件名,第二个参数为协议的版本号,默认为0.9 版本。__init__ 的步骤可以分为如下几个步骤:

  1. 依据协议版本初始化一些版本特征变量,包括协议标志位,是否对数据帧进行排序,是否有校验等。
  2. 定义start_elementend_elementchar_data 分别作为xml.parsers.expat 模块中的XMLParserType 对象的StartElementHandler,EndElementHandler, CharacterDataHandler的实现。然后开始对filename 指定的文件进行解析。
  3. 对解析完的数据进行后处理,包括对 payload 排序,计算crc_extra等。

这里我们看下调用MAVXML 构造函数后的对象的属性列表
MAVXML 属性列表
可以看到MAVXML 中从 XML 文件中解析出了很多信息,下面具体地描述解析过程。

3.2.1 依据协议版本初始化一些版本特征变量

首先是获得当前处理 XML 文件名及版本号,及初始化messageenuminclude属性为空,并设置了parse_time 属性为当前的日期。

代码段 4

186         self.filename = filename
187         self.basename = os.path.basename(filename)
188         if self.basename.lower().endswith(".xml"):
189             self.basename = self.basename[:-4]
190         self.basename_upper = self.basename.upper()
191         self.message = []
192         self.enum = []
193         # we use only the day for the parse_time, as otherwise
194         # it causes a lot of unnecessary cache misses with ccache
195         self.parse_time = time.strftime("%a %b %d %Y")
196         self.version = 2
197         self.include = []
198         self.wire_protocol_version = wire_protocol_version

说出三种解析XML文档的方式、然后依据协议的版本,设置一些 flag,分别是:

  • protocol_marker: 标志消息开始的字节
  • sort_fields:是否对payload 域进行排序
  • little_endian :字符的是否按最小字节先发送的传输顺序
  • crc_extra: 是否在checksum 的计算中加入 crc_extra
  • crc_struct: 是否把结构体包括在crc的计算中
  • command_24bit: message id 的范围是否允许超过256
  • allow_extension: 是否允许message 消息中存在扩展域

代码段 5

200         # setup the protocol features for the requested protocol version
201         if wire_protocol_version == PROTOCOL_0_9:
202             self.protocol_marker = ord('U')
203             self.sort_fields = False
204             self.little_endian = False
205             self.crc_extra = False
206             self.crc_struct = False
207             self.command_24bit = False
208             self.allow_extensions = False
209         elif wire_protocol_version == PROTOCOL_1_0:
210             self.protocol_marker = 0xFE
211             self.sort_fields = True
212             self.little_endian = True
213             self.crc_extra = True
214             self.crc_struct = False
215             self.command_24bit = False
216             self.allow_extensions = False
217         elif wire_protocol_version == PROTOCOL_2_0:
218             self.protocol_marker = 0xFD
219             self.sort_fields = True
220             self.little_endian = True
221             self.crc_extra = True
222             self.crc_struct = True
223             self.command_24bit = True
224             self.allow_extensions = True
225         else:
226             print("Unknown wire protocol version")
227             print("Available versions are: %s %s %s" % (PROTOCOL_0_9, PROTOCOL_1_0, PROTOCOL_2_0))
228             raise MAVParseError('Unknown MAVLink wire protocol version %s' % wire_protocol_version)

3.2.2 解析 XML 文件

这里先看一下 Pythonxml.parsers.expat 模块的官方文档中给出的例子

代码段 5

import xml.parsers.expat# 3 handler functions
def start_element(name, attrs):print('Start element:', name, attrs)
def end_element(name):print('End element:', name)
def char_data(data):print('Character data:', repr(data))p = xml.parsers.expat.ParserCreate()p.StartElementHandler = start_element
p.EndElementHandler = end_element
p.CharacterDataHandler = char_datap.Parse("""<?xml version="1.0"?>
<parent id="top"><child1 name="paul">Text goes here</child1>
<child2 name="fred">More text</child2>
</parent>""", 1)

结果输出为:

Start element: parent {'id': 'top'}
Start element: child1 {'name': 'paul'}
Character data: 'Text goes here'
End element: child1
Character data: '\n'
Start element: child2 {'name': 'fred'}
Character data: 'More text'
End element: child2
Character data: '\n'
End element: parent

从上面的输出可以看出,xml.parsers.expat 解析流程为:把 XML 文件从头到尾的所有标签按顺序遍历,对于标签的入口调用 StartElementHandler 方法,对于标签中的文本调用CharacterDataHandler方法,标签结束时则调用EndElementHandler方法。

源码。在 MAVXML 中定义了start_element(name, attrs)对应于标签入口函数的、char_data(data)对应于标签内文本处理函数,end_element(name)对应于标签出口函数。下面是标签入口函数start_element 的定义:

代码段 6

239         def start_element(name, attrs):
240             """ """
241             in_element_list.append(name)
242             in_element = '.'.join(in_element_list)
243             #print in_element
244             if in_element == "mavlink.messages.message":                                                                           245                 check_attrs(attrs, ['name', 'id'], 'message')
246                 self.message.append(MAVType(attrs['name'], attrs['id'], p.CurrentLineNumber))
247             elif in_element == "mavlink.messages.message.extensions":
248                 self.message[-1].extensions_start = len(self.message[-1].fields)
249             elif in_element == "mavlink.messages.message.field":
250                 check_attrs(attrs, ['name', 'type'], 'field')
251                 print_format = attrs.get('print_format', None)
252                 enum = attrs.get('enum', '')
253                 display = attrs.get('display', '')
254                 units = attrs.get('units', '')
255                 if units:
256                     units = '[' + units + ']'
257                 new_field = MAVField(attrs['name'], attrs['type'], print_format, self, enum=enum, display=display, units=units)
258                 if self.message[-1].extensions_start is None or self.allow_extensions:
259                     self.message[-1].fields.append(new_field)
260             elif in_element == "mavlink.enums.enum":
261                 check_attrs(attrs, ['name'], 'enum')
262                 self.enum.append(MAVEnum(attrs['name'], p.CurrentLineNumber))
263             elif in_element == "mavlink.enums.enum.entry":
264                 check_attrs(attrs, ['name'], 'enum entry')
265                 # determine value and if it was automatically assigned (for possible merging later)
266                 if 'value' in attrs:
267                     value = eval(attrs['value'])
268                     autovalue = False
269                 else:
270                     value = self.enum[-1].highest_value + 1
271                     autovalue = True
272                 # check lowest value
273                 if (self.enum[-1].start_value is None or value < self.enum[-1].start_value):
274                     self.enum[-1].start_value = value
275                 # check highest value
276                 if (value > self.enum[-1].highest_value):
277                     self.enum[-1].highest_value = value
278                 # append the new entry
279                 self.enum[-1].entry.append(MAVEnumEntry(attrs['name'], value, '', False, autovalue, self.filename, p.CurrentLineNum    ber))
280             elif in_element == "mavlink.enums.enum.entry.param":
281                 check_attrs(attrs, ['index'], 'enum param')
282                 self.enum[-1].entry[-1].param.append(
283                                                 MAVEnumParam(attrs['index'],
284                                                         label=attrs.get('label', ''), units=attrs.get('units', ''),
285                                                         enum=attrs.get('enum', ''), increment=attrs.get('increment', ''),
286                                                         minValue=attrs.get('minValue', ''),
287                                                         maxValue=attrs.get('maxValue', ''), default=attrs.get('default', '0'),
288                                                         reserved=attrs.get('reserved', False) ))

下面是出口函数end_element的定义

代码段 7

298         def end_element(name):
299             """"""
300             in_element_list.pop()

下面是标签内文本处理函数的定义,

代码段 8

302         def char_data(data):
303             in_element = '.'.join(in_element_list)
304             if in_element == "mavlink.messages.message.description":
305                 self.message[-1].description += data
306             elif in_element == "mavlink.messages.message.field":
307                 if self.message[-1].extensions_start is None or self.allow_extensions:
308                     self.message[-1].fields[-1].description += data
309             elif in_element == "mavlink.enums.enum.description":
310                 self.enum[-1].description += data
311             elif in_element == "mavlink.enums.enum.entry.description":
312                 self.enum[-1].entry[-1].description += data
313             elif in_element == "mavlink.enums.enum.entry.param":
314                 self.enum[-1].entry[-1].param[-1].description += data
315             elif in_element == "mavlink.version":
316                 self.version = int(data)
317             elif in_element == "mavlink.include":
318                 self.include.append(data)

如何在XML中调用资源文件?下面是上面所定义的handlers函数的绑定及XML 的解析函数的调用:

代码段 9

320         f = open(filename, mode='rb')
321         p = xml.parsers.expat.ParserCreate()
322         p.StartElementHandler = start_element
323         p.EndElementHandler = end_element
324         p.CharacterDataHandler = char_data
325         p.ParseFile(f)
326         f.close()

在解析过程中,通过in_element_list 维护着当前所解析的路径。例如,假设目前解析到 common.xml 下图红色箭头所示的标签处,
在这里插入图片描述
此时的in_element_list 的为['mavlink','messages','message'],当进一步解析,进入

<field type="uint8_t" name="system_status" enum="MAV_STATE"> System status flag. </field>

此时调用StartElementHandlerstart_element , 并传入参数name=field, attrs={'type':'uint8_t', 'name':'system_status', 'enum':'MAV_STATE'},
那么有in_element=mavlink.messages.message.field ,于是会创建一个MAVField 对象添加到messagefields里面。接着调用 CharacterDataHandlerchar_data 方法,此时传入data='System status flag.',然后该值会被添加到start_element 所创建的MAVField对象的descritption属性中。当处理完时,调用EndElementHandlerend_element 方法,从in_element_list 中弹出'field'

3.2.3 对解析后结果的后处理

后处理主要包括:

  1. 对以MAV_CMD 开头的且参数个数不足 7 的枚举类型把参数按默认值补足 7 个。
  2. 如果 MAVLink2.0 之前的版本则剔除消息 ID 大于 256 的消息
  3. 把消息的域按照大字节在前小字节在后进行重排(对于数组则按照其数据类型的大小,而不是按照其数组大小)。计算数据的总长度。
  4. 依据重排后的域计算crc_extra

netty源码剖析,最后把这些值放到以message_ 开头的变量里面

代码段 10

# 消息序号, 比如 heartbeat 为 0
427             key = m.id
# 上面提到的 crc_extra
428             self.message_crcs[key] = m.crc_extra
# 发送出去的消息总长度(包括extension部分)
429             self.message_lengths[key] = m.wire_length
# 不包括 extension 部分的消息的长度
430             self.message_min_lengths[key] = m.wire_min_length
# 消息的名称,例如 heartbeat 就是消息的名称
431             self.message_names[key] = m.name
# 消息中是否显示包含 target_system 和 target_component 的 flag
432             self.message_flags[key] = m.message_flags
# target_system 在消息重排之后的域中的偏移量,无论是否含有 taregt_system 域都默认为0
433             self.message_target_system_ofs[key] = m.target_system_ofs
# target_system 在消息重排之后在域中的偏离量,无论是否含有target_system 都默认为0
434             self.message_target_component_ofs[key] = m.target_component_ofs

最后检查了数据的长度是否超出常见的 radio 的传输长度(64)

代码段 11

439             if m.wire_length+8 > 64:
440                 print("Note: message %s is longer than 64 bytes long (%u bytes), which can cause fragmentation since many radio mod    ems use 64 bytes as maximum air transfer unit." % (m.name, m.wire_length+8))

如下图所示,这里的 8 是 PAYLOAD 的以外的字节的长度(图中的红色和绿色部分),wire_length 算出的是PAYLOAD 的长度, 总长度是 8 + wire_lengthMAVLink v1 Frame
但我们注意到这是针对 MAVLink v1 的判断,对于 MAVLink v2 并不成立(参考下图),红色加绿色的部分的长度明显大于 8 个字节。
在这里插入图片描述
在完成XML 数据解析之后就是目标代码生成。

4 目标代码生成

最后一步,根据 opts.language 所指定的语言生成目标代码

164     opts.language = opts.language.lower()
165     if opts.language == 'python':
166         from . import mavgen_python
167         mavgen_python.generate(opts.output, xml)
168     elif opts.language == 'c':
169         from . import mavgen_c
170         mavgen_c.generate(opts.output, xml)
171     elif opts.language == 'wlua':
172         from . import mavgen_wlua
173         mavgen_wlua.generate(opts.output, xml)
174     elif opts.language == 'cs':
175         from . import mavgen_cs
176         mavgen_cs.generate(opts.output, xml)
177     elif opts.language == 'javascript':
178         from . import mavgen_javascript
179         mavgen_javascript.generate(opts.output, xml)
180     elif opts.language == 'typescript':
181         from . import mavgen_typescript
182         mavgen_typescript.generate(opts.output, xml)
183     elif opts.language == 'objc':
184         from . import mavgen_objc
185         mavgen_objc.generate(opts.output, xml)
186     elif opts.language == 'swift':
187         from . import mavgen_swift
188         mavgen_swift.generate(opts.output, xml)
189     elif opts.language == 'java':
190         from . import mavgen_java
191         mavgen_java.generate(opts.output, xml)
192     elif opts.language == 'c++11':
193         from . import mavgen_cpp11
194         mavgen_cpp11.generate(opts.output, xml)
195     else:
196         print("Unsupported language %s" % opts.language)

pyi文件、这里我们看一下变量 optsxml 的内容
opts:

definitions: ['/mnt/d/github_/mavl...ommon.xml']
error_limit: 200
language: 'c'
output: '/mnt/d/github_/mavlink_cpp'
strict_units: False
validate: False
wire_protocol: '2.0'

xml :

[<pymavlink.generator.mavparse.MAVXML object at 0x7f5a30ec1fd0>]

xml 列表里面唯一一个MAVXML object 展开后有

allow_extensions: True
basename: 'common'
basename_upper: 'COMMON'
command_24bit: True
crc_extra: True
crc_struct: True
enum: [<pymavlink.generator...a30ed02b0>, <pymavlink.generator...a30ed06d8>, <pymavlink.generator...a30edf208>, <pymavlink.generator...a30edf278>, <pymavlink.generator...a30edf470>, <pymavlink.generator...a30edf898>, <pymavlink.generator...a30edfa90>, <pymavlink.generator...a30edfba8>, <pymavlink.generator...a30edfc18>, <pymavlink.generator...a30ee50b8>, <pymavlink.generator...a30ef0be0>, <pymavlink.generator...a30efb3c8>, <pymavlink.generator...a30efb630>, <pymavlink.generator...a30efba58>, ...]
filename: '/mnt/d/github_/mavlink/message_definitions/v1.0/common.xml'
include: []
largest_payload: 255
little_endian: True
message: [<pymavlink.generator...a30058dd8>, <pymavlink.generator...a3005e2e8>, <pymavlink.generator...a3005e9b0>, <pymavlink.generator...a3005eb00>, <pymavlink.generator...a3005edd8>, <pymavlink.generator...a30063208>, <pymavlink.generator...a300634a8>, <pymavlink.generator...a30063630>, <pymavlink.generator...a30063cf8>, <pymavlink.generator...a30063eb8>, <pymavlink.generator...a300671d0>, <pymavlink.generator...a30067358>, <pymavlink.generator...a300675f8>, <pymavlink.generator...a30067978>, ...]
message_crcs: {0: 50, 1: 124, 2: 137, 4: 237, 5: 217, 6: 104, 7: 119, 8: 117, 11: 89, 20: 214, 21: 159, 22: 220, 23: 168, 24: 24, ...}
message_flags: {0: 0, 1: 0, 2: 0, 4: 3, 5: 1, 6: 0, 7: 0, 8: 0, 11: 1, 20: 3, 21: 3, 22: 0, 23: 3, 24: 0, ...}
message_lengths: {0: 9, 1: 31, 2: 12, 4: 14, 5: 28, 6: 3, 7: 32, 8: 36, 11: 6, 20: 20, 21: 2, 22: 25, 23: 23, 24: 52, ...}
message_min_lengths: {0: 9, 1: 31, 2: 12, 4: 14, 5: 28, 6: 3, 7: 32, 8: 36, 11: 6, 20: 20, 21: 2, 22: 25, 23: 23, 24: 30, ...}
message_names: {0: 'HEARTBEAT', 1: 'SYS_STATUS', 2: 'SYSTEM_TIME', 4: 'PING', 5: 'CHANGE_OPERATOR_CONTROL', 6: 'CHANGE_OPERATOR_CONTROL_ACK', 7: 'AUTH_KEY', 8: 'LINK_NODE_STATUS', 11: 'SET_MODE', 20: 'PARAM_REQUEST_READ', 21: 'PARAM_REQUEST_LIST', 22: 'PARAM_VALUE', 23: 'PARAM_SET', 24: 'GPS_RAW_INT', ...}
message_target_component_ofs: {0: 0, 1: 0, 2: 0, 4: 13, 5: 0, 6: 0, 7: 0, 8: 0, 11: 0, 20: 3, 21: 1, 22: 0, 23: 5, 24: 0, ...}
message_target_system_ofs: {0: 0, 1: 0, 2: 0, 4: 12, 5: 0, 6: 0, 7: 0, 8: 0, 11: 4, 20: 2, 21: 0, 22: 0, 23: 4, 24: 0, ...}
parse_time: 'Fri Mar 27 2020'
protocol_marker: 253
sort_fields: True
version: 3
wire_protocol_version: '2.0'
__len__: 1

具体的生成过程见:
链接 >>> pymavlink 源码剖析(二)之生成代码

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/77793.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息