cmake:cmake-buildsystem

1059 篇文章 280 订阅

介绍

基于cmake的构建系统被组织为一组高级逻辑目标。每个目标对应于一个可执行文件或者库,或者是一个包含自定义命令的自定义目标。目标之间的依赖关系在buildsystem中表示,以确定生成顺序和响应更改的重新生成规则

二进制目标

可执行文件和库是使用add_executable()和add_library()命令定义的。生成的二进制文件是具有针对目标平台的适当前缀、后缀和扩展名。二进制目标之间的依赖关系使用target_link_libraries()命令表示:

add_library(archive archive.cpp zip.cpp lzma.cpp)
add_executable(zipapp zipapp.cpp)
target_link_libraries(zipapp archive)
  • archive被定义为一个静态库—一个包含从archive.cpp、 zip.cpp、 lzma.cpp编译的对象的归档。
  • zipapp被定义为听过编译和链接zipapp.cpp而形成的可执行文件。
  • 当链接zipapp可执行文件时,将链接archive静态库

二进制可执行文件

add_executable()命令定义了一个可执行的目标:

add_executable(mytool mytool.cpp)

add_custom_command()之类的命令可以透明的将EXECUTABLE目标用作COMMAND可执行文件,它生成在构建时运行的规则。构建系统规则将确保在尝试运行命令之前构建可执行文件

二进制库类型

正常库

除非指定了类型,否则add_library()默认定义静态库。指定动态库还是静态库的命令如下:

add_library(archive SHARED archive.cpp zip.cpp lzma.cpp)
add_library(archive STATIC archive.cpp zip.cpp lzma.cpp)

默认情况下,可以启用BUILD_SHARED_LIBS变量来改变add_library()的行为来构建共享库。

在构建系统定义作为一个整体的上下文中,特定的库是共享的还是静态的很大程序上是无关紧要的------命令、依赖规范和其他API的工作方式与库类型无关。MODULE库类型的不同之处在于它通常没有被链接到------它没有被用在target_link_libraries()命令的右侧。它是一种使用运行时技术作为插件加载的类型。如果库没有导出任何非托管符号(例如Windows资源DLL, c++ /CLI DLL),则要求该库不是共享库,因为CMake期望共享库至少导出一个符号。

add_library(archive MODULE 7z.cpp)

Apple Frameworks

  • 一个共享库可以被标记为【FRAMEWORK】目标属性来创建一个macOS或iOS框架包。
  • 带有FRAMEWORK目标属性的库也应该设置【FRAMEWORK_VERSION】目标属性。macOS约定通常将此属性设置为“A”值。
  • 【MACOSX_FRAMEWORK_IDENTIFIER】设置CFBundleIdentifier键值,它唯一地标识bundle。
add_library(MyFramework SHARED MyFramework.cpp)
set_target_properties(MyFramework PROPERTIES
  FRAMEWORK TRUE
  FRAMEWORK_VERSION A # Version "A" is macOS convention
  MACOSX_FRAMEWORK_IDENTIFIER org.cmake.MyFramework
)

OBJECT 库

OBJECT库类型定义了编译给定源文件所产生的对象文件的非归档集合。对象文件集合可以使用$语法作为其他目标的源输入。这是一个生成器表达式,可用于向其他目标提供OBJECT库内容:

add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)

add_library(archiveExtras STATIC $<TARGET_OBJECTS:archive> extras.cpp)

add_executable(test_exe $<TARGET_OBJECTS:archive> test.cpp)

那些其他目标的链接(或归档)步骤除了使用来自它们自己源的文件外,还将使用目标文件集合。

或者,对象库可以链接到其他目标:

add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)

add_library(archiveExtras STATIC extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)

add_executable(test_exe test.cpp)
target_link_libraries(test_exe archive)

那些其他目标的链接(或存档)步骤将使用直接链接的object库中的对象文件。此外,在编译其他目标中的源代码时,OBJECT库的使用需求将得到尊重。此外,这些使用需求将传递到其他目标的依赖项。

在使用add_custom_command(TARGET)命令签名时,不能将对象库用作目标。但是,add_custom_command(OUTPUT)或file(GENERATE)可以使用$来使用对象列表。

  • CMake的library目标,有一个OBJECT模式,这个模式的library不会被archive成静态库,而是一个“假的”库,产物是一堆对象文件。
add_library(my_library OBJECT foo.cpp bar.cpp baz.cpp)
  • 使用时比较特别,你必须使用“这个目标的对象文件”的生成表达式,放在add_executable、add_library放源代码的位置,而不是target_link_library的位置:
add_executable(my_app my_app.cpp $<TARGET_OBJECTS:my_library>)
add_library(my_static_lib STATIC some_other_source.cpp $<TARGET_OBJECTS:my_library>)
  • 不过这种需求并不太寻常,大部分情况下你可以用静态库目标来做同样的事情,因为静态库实际上就是对象文件打包,没有做特别的事情。我能想到的唯一必须这么做的理由,就是你想把项目分成几个部分,而这几个部分之间的符号依赖是一锅粥,打包成静态库之后,链接时顺序搞不定。

构建规范和使用要求

target_include_directories()、target_compile_definitions()和target_compile_options()命令指定了二进制目标的构建规范和使用要求。这些命令分别填充INCLUDE_DIRECTORIES、COMPILE_DEFINITIONS和COMPILE_OPTIONS目标属性,以及/或INTERFACE_INCLUDE_DIRECTORIES、INTERFACE_COMPILE_DEFINITIONS和INTERFACE_COMPILE_OPTIONS目标属性。

每个命令都有一个PRIVATE、PUBLIC和INTERFACE【interface_接口)】模式。

  • PRIVATE模式仅填充目标属性的非INTERFACE_变体,
  • INTERFACE模式仅填充INTERFACE_变体。
  • PUBLIC模式填充各自目标属性的两个变体。

每个命令可以通过多个关键字来调用:

target_compile_definitions(archive
  PRIVATE BUILDING_WITH_LZMA
  INTERFACE USING_ARCHIVE_LIB
)

请注意,使用需求并不是为了方便而让下游使用特定的COMPILE_OPTIONS或compile_definition等。属性的内容必须是要求,而不仅仅是建议或方便。

请参阅cmake-packages(7)手册的“创建可重定位包”一节,讨论在为重分配创建包时指定使用要求时必须注意的额外事项。

目标属性(Target Properties)

当编译二进制目标的源文件时,INCLUDE_DIRECTORIES, COMPILE_DEFINITIONS和COMPILE_OPTIONS目标属性的内容被适当地使用。

  • INCLUDE_DIRECTORIES中的条目以-I或- system前缀和属性值中出现的顺序添加到编译行。
  • COMPILE_DEFINITIONS中的条目以-D或/D作为前缀,并以未指定的顺序添加到编译行中。DEFINE_SYMBOL目标属性也被添加为编译定义,作为SHARED和MODULE库目标的一种特殊方便的情况。
  • COMPILE_OPTIONS中的条目被转义为shell,并按属性值中出现的顺序添加。有几个编译选项有特殊的单独处理,比如POSITION_INDEPENDENT_CODE。

INTERFACE_INCLUDE_DIRECTORIES、INTERFACE_COMPILE_DEFINITIONS和INTERFACE_COMPILE_OPTIONS目标属性的内容是使用需求——它们指定用户必须使用的内容,以便正确编译并与显示它们的目标链接。对于任何二进制目标,target_link_libraries()命令中指定的每个目标上的每个INTERFACE_属性的内容都会被消耗:

set(srcs archive.cpp zip.cpp)
if (LZMA_FOUND)
  list(APPEND srcs lzma.cpp)
endif()
add_library(archive SHARED ${srcs})
if (LZMA_FOUND)
  # The archive library sources are compiled with -DBUILDING_WITH_LZMA
  target_compile_definitions(archive PRIVATE BUILDING_WITH_LZMA)
endif()
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)

add_executable(consumer)
# Link consumer to archive and consume its usage requirements. The consumer
# executable sources are compiled with -DUSING_ARCHIVE_LIB.
target_link_libraries(consumer archive)

因为通常需要将源目录和相应的构建目录添加到INCLUDE_DIRECTORIES中,所以可以启用CMAKE_INCLUDE_CURRENT_DIR变量来方便地将相应的目录添加到所有目标的INCLUDE_DIRECTORIES中。可以启用变量CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE,将相应的目录添加到所有目标的INTERFACE_INCLUDE_DIRECTORIES中。通过使用target_link_libraries()命令,可以方便地使用多个不同目录中的目标。

可传递的使用要求

目标的使用需求可以传递到依赖项。target_link_libraries()命令具有PRIVATE、INTERFACE和PUBLIC关键字来控制传播。

add_library(archive archive.cpp)
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)

add_library(serialization serialization.cpp)
target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)

add_library(archiveExtras extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)
target_link_libraries(archiveExtras PRIVATE serialization)
# archiveExtras is compiled with -DUSING_ARCHIVE_LIB
# and -DUSING_SERIALIZATION_LIB

add_executable(consumer consumer.cpp)
# consumer is compiled with -DUSING_ARCHIVE_LIB
target_link_libraries(consumer archiveExtras)

因为archive是archiveExtras的公共依赖项,所以它的使用需求也会传播给消费者。因为序列化是archiveExtras的私有依赖项,所以它的使用需求不会传播给消费者。

  • 通常,如果依赖项只在库的实现中使用,而不是在头文件中使用,则应该使用target_link_libraries()和PRIVATE关键字指定依赖项。
  • 如果一个依赖项在库的头文件中被额外使用(例如用于类继承),那么它应该被指定为PUBLIC依赖项。
  • 一个库的实现没有使用的依赖项,而只有它的头文件使用的依赖项,应该被指定为一个INTERFACE依赖项。

target_link_libraries()命令可以通过多个关键字调用:

target_link_libraries(archiveExtras
  PUBLIC archive
  PRIVATE serialization
)

通过从依赖项中读取目标属性的INTERFACE_变量并将值附加到操作数的非INTERFACE_变量来传播。

例如,读取依赖项的INTERFACE_INCLUDE_DIRECTORIES并将其附加到操作数的INCLUDE_DIRECTORIES。如果顺序是相关的,并且由target_link_libraries()调用产生的顺序不允许正确的编译,那么使用适当的命令直接设置属性可以更新顺序。

例如,如果目标的链接库必须按顺序lib1 lib2 lib3指定,但包含目录必须按顺序lib3 lib1 lib2指定:

target_link_libraries(myExe lib1 lib2 lib3)
target_include_directories(myExe
  PRIVATE $<TARGET_PROPERTY:lib3,INTERFACE_INCLUDE_DIRECTORIES>)

请注意,在指定目标的使用要求时必须小心,这些目标将使用install(EXPORT)命令导出用于安装。有关更多信息,请参见创建包

兼容的接口属性

一些目标属性需要在目标和每个依赖项的接口之间兼容。比如,POSITION_INDEPENDENT_CODE 目标属性可以指定一个bool值,即是否应该将目标编译为位置独立的代码,这具有特定于平台的结果。目标还可以指定使用要求INTERFACE_POSITION_INDEPENDENT_CODE来通信,即消费者必须编译为位置无关码

add_executable(exe1 exe1.cpp)
set_property(TARGET exe1 PROPERTY POSITION_INDEPENDENT_CODE ON)

add_library(lib1 SHARED lib1.cpp)
set_property(TARGET lib1 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1)

这里,exe1和exec2都被编译为位置无关码。lib1也将编译为位置无关码,因为这是共享库的默认设置。如果依赖是有冲突的,不兼容的需求,cmake会发出一个诊断

add_library(lib1 SHARED lib1.cpp)
set_property(TARGET lib1 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)

add_library(lib2 SHARED lib2.cpp)
set_property(TARGET lib2 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE OFF)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1)
set_property(TARGET exe1 PROPERTY POSITION_INDEPENDENT_CODE OFF)

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1 lib2)

lib1要求INTERFACE_POSITION_INDEPENDENT_CODE与exe1目标的POSITION_INDEPENDENT_CODE属性不“兼容”。库要求消费者构建为位置无关的代码,而可执行文件指定不构建为位置无关的代码,因此会发出诊断。

lib1和lib2需求不“兼容”。其中一个要求消费者是作为位置无关的代码构建的,而另一个要求消费者不是作为位置无关的代码构建的。因为exe2链接到两者,并且它们在冲突中,一个CMake错误消息被发出:

CMake Error: The INTERFACE_POSITION_INDEPENDENT_CODE property of "lib2" does
not agree with the value of POSITION_INDEPENDENT_CODE already determined
for "exe2".

要做到“兼容”,POSITION_INDEPENDENT_CODE属性(如果设置了该属性)必须在布尔意义上与所有传递指定的依赖项的INTERFACE_POSITION_INDEPENDENT_CODE属性相同。

通过在COMPATIBLE_INTERFACE_BOOL目标属性的内容中指定该属性,“兼容接口需求”的这个属性可以扩展到其他属性。每个指定的属性必须在消费目标和每个依赖项中带有INTERFACE_前缀的相应属性之间兼容:

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY INTERFACE_CUSTOM_PROP ON)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_BOOL CUSTOM_PROP
)

add_library(lib1Version3 SHARED lib1_v3.cpp)
set_property(TARGET lib1Version3 PROPERTY INTERFACE_CUSTOM_PROP OFF)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1Version2) # CUSTOM_PROP will be ON

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1Version2 lib1Version3) # Diagnostic

非布尔属性也可以参与“兼容接口”的计算。在COMPATIBLE_INTERFACE_STRING属性中指定的属性必须是未指定的,或者在所有传递指定的依赖项中与相同的字符串进行比较。这有助于确保库的多个不兼容版本不会通过目标的传递需求链接到一起:

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY INTERFACE_LIB_VERSION 2)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_STRING LIB_VERSION
)

add_library(lib1Version3 SHARED lib1_v3.cpp)
set_property(TARGET lib1Version3 PROPERTY INTERFACE_LIB_VERSION 3)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1Version2) # LIB_VERSION will be "2"

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1Version2 lib1Version3) # Diagnostic

COMPATIBLE_INTERFACE_NUMBER_MAX目标属性指定内容将以数字方式计算,并计算所有指定内容中的最大数量:

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY INTERFACE_CONTAINER_SIZE_REQUIRED 200)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_NUMBER_MAX CONTAINER_SIZE_REQUIRED
)

add_library(lib1Version3 SHARED lib1_v3.cpp)
set_property(TARGET lib1Version3 PROPERTY INTERFACE_CONTAINER_SIZE_REQUIRED 1000)

add_executable(exe1 exe1.cpp)
# CONTAINER_SIZE_REQUIRED will be "200"
target_link_libraries(exe1 lib1Version2)

add_executable(exe2 exe2.cpp)
# CONTAINER_SIZE_REQUIRED will be "1000"
target_link_libraries(exe2 lib1Version2 lib1Version3)

类似地,可以使用COMPATIBLE_INTERFACE_NUMBER_MIN从依赖项计算属性的最小数值。

每个计算出的“兼容的”属性值可以在生成时使用生成器表达式在消费者中读取。

请注意,对于每个依赖项,在每个兼容接口属性中指定的属性集不能与任何其他属性中指定的属性集相交。

Property Origin Debugging

因为构建规范可以由依赖关系决定,缺少创建目标的代码和负责设置构建规范的代码的局地性可能会使得代码更难以推理。cmake提供了一个调试工具来打印属性内容来源,这些属性可能由依赖关系决定。可以调试的属性在CMAKE_DEBUG_TARGET_PROPERTIES变量文档中列出:

set(CMAKE_DEBUG_TARGET_PROPERTIES
  INCLUDE_DIRECTORIES
  COMPILE_DEFINITIONS
  POSITION_INDEPENDENT_CODE
  CONTAINER_SIZE_REQUIRED
  LIB_VERSION
)
add_executable(exe1 exe1.cpp)

对于在COMPATIBLE_INTERFACE_BOOL或COMPATIBLE_INTERFACE_STRING中列出的属性,调试输出显示了哪个目标负责设置该属性,以及哪个其他依赖项也定义了该属性。在COMPATIBLE_INTERFACE_NUMBER_MAX和COMPATIBLE_INTERFACE_NUMBER_MIN的情况下,调试输出显示每个依赖项的属性值,以及该值是否决定了新的极限。

使用生成器表达式构建规范

构建规范可以使用包含有条件内容或仅在生成时已知内容的生成器表达式。例如,一个属性的“compatible”值可以用TARGET_PROPERTY表达式读取:

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY
  INTERFACE_CONTAINER_SIZE_REQUIRED 200)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_NUMBER_MAX CONTAINER_SIZE_REQUIRED
)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1Version2)
target_compile_definitions(exe1 PRIVATE
    CONTAINER_SIZE=$<TARGET_PROPERTY:CONTAINER_SIZE_REQUIRED>
)

在本例中,将使用-DCONTAINER_SIZE=200编译exe1源文件。

可以使用CONFIG生成器表达式方便地设置配置确定的构建规范。

target_compile_definitions(exe1 PRIVATE
    $<$<CONFIG:Debug>:DEBUG_BUILD>
)

链接库和生成器表达式

与构建规范一样,可以使用生成器表达式条件来指定链接库。然而,由于使用需求的消耗是基于来自链接依赖项的收集,因此存在一个额外的限制,即链接依赖项必须形成一个“有向无环图”。也就是说,如果对目标的链接依赖于目标属性的值,则该目标属性可能不依赖于所链接的依赖项:

add_library(lib1 lib1.cpp)
add_library(lib2 lib2.cpp)
target_link_libraries(lib1 PUBLIC
  $<$<TARGET_PROPERTY:POSITION_INDEPENDENT_CODE>:lib2>
)
add_library(lib3 lib3.cpp)
set_property(TARGET lib3 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1 lib3)

由于exe1目标的POSITION_INDEPENDENT_CODE属性的值依赖于被链接的库(lib3),而链接exe1的边缘由相同的POSITION_INDEPENDENT_CODE属性决定,上面的依赖关系图包含了一个循环。Cmake(1)发出一个错误消息。

输出构件

由add_library()和add_executable()命令创建的构建系统目标创建规则来创建二进制输出。二进制文件的准确输出位置只能在生成时确定,因为它可以依赖于链接依赖的构建配置和链接语言等。TARGET_FILE、TARGET_LINKER_FILE和相关表达式可以用来访问生成的二进制文件的名称和位置。然而,这些表达式并不适用于OBJECT库,因为这些库生成的文件中没有一个是与表达式相关的。

有三种类型的输出工件可以由目标构建,如下面的部分中详细介绍。它们的分类在DLL平台和非DLL平台之间是不同的。包括Cygwin在内的所有基于windows的系统都是DLL平台。

运行时输出构件

构建系统目标的运行时输出构件可能是:

  • 由add_executable()命令创建的可执行目标的可执行文件(例如.exe)。
  • 在DLL平台上:由add_library()命令和shared选项创建的共享库目标的可执行文件(例如. DLL)。

RUNTIME_OUTPUT_DIRECTORY和RUNTIME_OUTPUT_NAME目标属性可以用于控制构建树中运行时输出构件的位置和名称。

库输出构件

构建系统目标的库输出构件可能是:

  • 模块库目标的可加载模块文件(例如.dll或.so),由add_library()命令和module选项创建。
  • 在非dll平台上:由add_library()命令和shared选项创建的共享库目标的共享库文件(例如.so或.dylib)。

LIBRARY_OUTPUT_DIRECTORY和LIBRARY_OUTPUT_NAME目标属性可以用来控制库输出构件在构建树中的位置和名称。

档案输出构件

构建系统目标的存档输出构件可能是:

  • 由add_library()命令和static选项创建的静态库目标的静态库文件(例如.lib或.a)。
  • 在DLL平台上:通过add_library()命令和shared选项创建的共享库目标的导入库文件(例如.lib)。只有当库导出至少一个非托管符号时,才保证此文件存在。
  • 在DLL平台上:当设置了ENABLE_EXPORTS目标属性时,由add_executable()命令创建的可执行目标的导入库文件(例如.lib)。
  • 在AIX上:当设置了ENABLE_EXPORTS目标属性时,由add_executable()命令创建的可执行目标的连接器导入文件(例如.imp)。

ARCHIVE_OUTPUT_DIRECTORY和ARCHIVE_OUTPUT_NAME目标属性可以用来控制归档输出工件在构建树中的位置和名称。

Directory-Scoped命令

target_include_directories()、target_compile_definitions()和target_compile_options()命令一次只能对一个目标生效。命令add_compile_definitions()、add_compile_options()和include_directories()具有类似的功能,但是为了方便起见,它们是在目录作用域而不是目标作用域操作的。

伪目标

有些目标类型并不表示构建系统的输出,而是只表示外部依赖项、别名或其他非构建构件等输入。生成的构建系统中不表示伪目标。

导入目标

导入的目标表示预先存在的依赖项。通常这样的目标是由上游包定义的,应该被视为不可变的。在声明一个导入的目标之后,可以通过使用习惯的命令,如target_compile_definitions()、target_include_directories()、target_compile_options()或target_link_libraries()来调整它的目标属性,就像其他常规目标一样。

导入的目标可能具有与二进制目标相同的使用需求属性,例如INTERFACE_INCLUDE_DIRECTORIES、INTERFACE_COMPILE_DEFINITIONS、INTERFACE_COMPILE_OPTIONS、INTERFACE_LINK_LIBRARIES和INTERFACE_POSITION_INDEPENDENT_CODE。

LOCATION也可以从导入的目标读取,尽管很少有这样做的理由。像add_custom_command()这样的命令可以透明地使用导入的可执行目标作为COMMAND可执行文件。

导入目标定义的范围是定义它的目录。它可以从子目录访问和使用,但不能从父目录或兄弟目录访问和使用。作用域类似于cmake变量的作用域。

也可以定义一个在构建系统中可以全局访问的全局导入目标。

关于使用导入的目标创建包的更多信息,请参阅cmake-packages(7)手册

别名目标

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值