越过边界

1994年,MVS 4.3(z / OS的旧版本)推出了一些新功能:UNIX。 突然之间,没有z / OS知识的UNIX程序员可以将应用程序从其他UNIX系统移植到大型机,并以他们喜欢的语言(如C和Java™)创建代码,并且都在熟悉的环境中进行。 这也意味着本机z / OS程序员现在可以使用UNIX服务执行任何操作,从使用TCPIP进行通信到访问UNIX文件甚至调用UNIX程序。 但是今天,大多数人仍然认为,UNIX系统服务(USS)与更传统的z / OS之间存在“隔离墙”,认为应用程序只能在一个或另一个上运行。 这根本是不正确的。 z / OS能够使任何应用程序使用两者的服务。

本文讨论如何编写跨界应用程序-访问USS服务的传统z / OS应用程序,或访问传统z / OS服务的USS程序。 本文主要讨论两种语言:高级汇编程序(HLASM,仍在传统的z / OS中使用)和C(UNIX程序员的最爱)。 它还提到了一些与C有关的特定于z / OS的问题和技术。本文将z / OS的UNIX部分称为USS(UNIX系统服务),将z / OS的“传统”非USS部分称为“本机” z / OS。

创建和编译C

C程序实际上只是普通的z / OS高级语言程序,无论它们在何处生成或运行。 它们都需要创建,编译和绑定。 让我们首先谈谈编译C代码。

z / OS C编译器实际上既是C编译器又是C ++编译器,可以从本机z / OS或USS进行调用。 z / OS上的C编译器是C89编译器,UNIX程序员知道它比其他UNIX系统上可用的编译器要旧一些。 您可以运行以下编译器:

  • 在USS中使用C, C++c89命令(它们都是一样的)
  • 在本地z / OS中使用标准ISPF面板(如果由Systems Programmer安装)
  • 使用CCCXX提供的REXX exec在TSO / E中的本机z / OS中运行
  • 在本机z / OS中批量

要告诉编译器您的程序是C还是C ++,这是您需要做的:

  • USS:用正确的扩展名命名程序文件: program.c (小写C)表示它为C, program.C (大写C)表示它为C ++。
  • 本机z / OS批处理:对C ++程序使用C编译器CXX运行时选项。 C编译器SCCNPRC数据集包含可从批处理中调用的两组过程,以编译代码:CBC *(用于C ++),EBC *(用于C)。
  • 本机z / OS TSO / E:对于C使用CC REXX exec,对于C ++使用CXX REXX exec

让我们看一下使用C时的一些问题。

代码位置

这并不奇怪,但是您的C代码(和头文件)可以驻留在USS文件或本机z / OS数据集中(PDS,PDS / E或Sequential,并且这些格式可以是固定或可变的记录格式)。 无论您从何处编译代码,这都有效:

  • 您可以从USS的本机z / OS数据集中编译C代码; 您只需要使用特殊的USS“ //”格式来指定本机z / OS顺序数据集。 这是一个USS Shell命令的示例,用于在PDS MYHLQ.PGMS.C:编译程序PGM1 MYHLQ.PGMS.C:

    c89 -o pgm1.o "//'MYHLQ.PGMS.C(PGM1)'"

  • 您可以通过在SYSIN DD语句上指定PATH来批量编译来自USS文件的C代码,这与清单1中的示例相似
    清单1.在USS文件中编译C代码的JCL
    //COMPILE EXEC CCNC001,                                    
    //SYSIN   DD  PATH='/u/mydir/pgms/pgm1.c',          
    //             PATHDISP=(KEEP,KEEP),PATHOPTS=(ORDONLY)

如果选择将代码存储在本机z / OS数据集中,则可能需要使用类似于RECFM = VB,LRECL = 256的DCB设置来分配代码(尤其是从另一个UNIX移植时)。 您不必将代码限制为1到72列,例如COBOL和HLASM。

从C访问z / OS设施

C程序可以使用标准C函数访问许多z / OS工具,而不管它们是在USS还是本机z / OS中运行。 例如,C函数sleep()大致等同于HLASM宏STIMER. C编译器还为您提供了一些z / OS特定的功能,例如__cabend() ,它与HLASM ABEND宏相同。 《 z / OS XL C / C ++运行时库参考》中记录了所有C / C ++函数。

需要注意的一个有趣事情是,您可以从不在USS下运行的程序中调用USS服务(例如getpid()fork() )。 在这种情况下,地址空间会自动“配音”,这是一种很好的说法,可以说它看起来像是USS内核的进程。

如果您需要访问z / OS控制块,则在本机z / OS和USS中使用C进行此操作都没有问题。 坏消息是IBM®不提供映射这些控制块的C头文件。 但这并不是个坏消息:C编译器还带有DSECT转换实用程序,可以将HLASM DSECT转换为C声明。 清单2是从z / OS扩展CVT控制块获取z / OS Sysplex名称的代码示例。

清单2.用于访问z / OS控制块的示例C代码
/* -----------------------------------------------------------  
       Map z/OS Control Block Structures                                
       ---------------------------------------------------------- */
    /* --- Cut down PSA Structure - just PSACVT to find our CVT - */
    struct psa {                                                        
       int psastuff[4];                /* 4 bytes before CVT Ptr  */  
       struct cvt *psacvt;      
       /* Ignore the rest of the PSA */      
    };
         
    /* --- Cut down CVT Structure - just CVTECVT to find ECVT --- */
    struct cvt {    
       char cvtstuff[140];             /* 140 bytes before ptr    */
       struct ecvt *cvtecvt;           /* Ptr to ECVT             */
       /* Ignore the rest of the CVT */  
    };
                          
    /* --- Cut down ECVT Structure - just ECVTSPLX to get Sysplex */
    struct ecvt {            
       char ecvtecvt[8];               /* 8 Bytes before Sysplex  */
       char ecvtsplx[8];               /* Sysplex Name            */
       /* Ignore the rest of the ECVT */     
    };   
              
    /* -----------------------------------------------------------  
       Variables             
       ---------------------------------------------------------- */
    struct psa *psa_ptr = 0;           /* PSA starts at address 0 */
    char sysplex[10];       
                      
    /* -----------------------------------------------------------  
       Put our Sysplex name into the sysplex variable          
       ---------------------------------------------------------- */
    strncpy(sysplex,psa_ptr->psacvt->cvtecvt->ecvtsplx,8);

在USS上从C访问z / OS文件

从C访问本机z / OS文件与访问USS文件没有太大不同。 普通的C文件功能,例如fopen(),fgets(), ferror(),fwrite()fclose()对于所有本机z / OS数据集以及UNIX文件都可以正常工作。 使用这些功能时要记住的事情是:

  • fopen()函数用于本机z / OS数据集时,必须使用“ //”格式(例如: //'MYHLQ.MYDATASET' )或先前分配的DDName来指定z / OS数据集名称。 DD: ddname格式的数据集。 但是请记住, fopen()仅适用于顺序数据集。 如果您有PDS / PDSE,则还需要指定成员名称(例如: //'MYHLQ.MYDATASET(MEMBER)'). ( //'MYHLQ.MYDATASET(MEMBER)').
  • 请记住,本机z / OS文件是基于记录的。 《 IBM z / OS C / C ++运行时库参考》有时引用记录I / O。 在这里谈论本地z / OS数据集。 因此,对于固定记录格式,您将获得整个记录,包括尾随空白。
  • 普通的C文件功能也适用于VSAM文件。 z / OS还包括一些VSAM特定的功能,例如flocate()fupdate().

从C调用汇编程序

z / OS上的C程序(本机z / OS和USS)都在LanguageEnvironment®(LE)(z / OS附带的一组库或运行时库)中运行。 汇编程序模块通常不这样做,这意味着链接(参数和调用/返回地址的传递方式)将有所不同。 因此,在C代码中定义HLASM程序时,您将必须包含#pragma语句,以定义不同的链接。 清单3显示了定义这种HLASM程序的C代码片段。

清单3.调用HLASM程序的C Pragma语句
#pragma linkage (PGM1,OS)        /* Non-LE Linkage      */                     
extern int PGM1(void *, int *);  /* Function definition */

但是,如果您的HLASM程序符合LE标准,则不需要此#pragma语句。

除此之外,定义和调用HLASM模块与任何其他模块相同。

问题和提示

  1. 如果编写的C代码需要在包括USS在内的不同UNIX系统上进行编译和运行,那么您会喜欢__MVS__宏。 使用此代码,您可以为条件编译编写#ifdef语句。 清单4显示了如何执行此操作。
  2. USS和本机z / OS之间的默认C / C ++编译器选项有所不同。 因此,请仔细检查您使用的选项。
  3. z / OS C / C ++编译器还提供了一种工具,可为您提供一种编写在语言环境下无法运行的C代码的方法,该方法可方便地用C代码替换HLASM出口。 它称为z / OS 1.9之前的z / OS版本的系统编程工具(SPC),以及z / OS 1.9和更高版本的MetalC。

    请注意,使用此功能只能为您提供运行时库功能的子集。 您可以在《 IBM z / OS XL C / C ++编程指南》 (对于SPC)或《 z / OS Metal C编程指南和参考》 (对于C Metal)中找到更多信息。

  4. 除非您指定C编译器ASCII选项,否则USS中运行的所有C代码都位于EBCDIC中(而地球上的所有其他UNIX系统都使用ASCII),因此任何字符串常量都被视为EBCDIC。 当您认为所有USS本身(包括shell命令输入和系统函数调用的结果)都在EBCDIC中运行时,这是有道理的。 如果编写Java JNI代码,这将特别有意义。 您需要将从Java传递的所有信息转换为EBCDIC。
  5. 当使用要外部化的函数或变量(包括Java JNI代码)创建DLL时,必须导出函数或变量;否则,必须导出函数或变量。 默认情况下不执行此操作。 可以使用#pragma export指令或C编译器exportall选项导出所有内容来完成此操作。
  6. 在z / OS上进行编译时,UNIX C程序员会遇到一个他们从未听说过的术语: XPLINK。 额外性能链接(XPLINK)模块是z / OS模块,具有在调用者之间传递参数和信息的不同方法。 它是z / OS的新功能,并且被宣传为比旧的本机z / OS链接要快得多。 但是XPLINK模块有很大的优势:您通常不能从XPLINK程序调用非XPLINK程序,反之亦然。 为此,您需要一个HLASM“胶水”模块。

    而且,XPLINK仅支持C和Java应用程序(尽管您也可以编写LE XPLINK HLASM程序)。 但是,如果您要编写64位程序,则别无选择。 您必须是XPLINK。

    要创建XPLINK程序,您必须:

    • 指定XPLINK C编译器选项。
    • 如果在本机z / OS中进行编译,请指定GOFF C编译器选项。
    • 如果在USS中进行绑定,请指定XPLINK绑定器选项。
    • 如果在本机z / OS中绑定,则添加DYNAM=DLLRENT绑定器选项(所有XPLINK程序必须是DLL )。
    • 如果在本机z / OS中绑定,请包括语言环境端文件 CELHS001和CELHS003。 有关包含这两个文件的示例JCL,请参见清单9

    您可以在《 IBM z / OS C / C ++编程指南》或《 XPLink:OS / 390 Extra Performance Linkage Redbook》中找到有关XPLINK的更多信息。

清单4.使用__MVS__进行条件编译的C代码
#if defined(__MVS__)
/* (z/OS specific code) */
#endif

创建和组装HLASM

本地HLASM程序员将熟悉使用标准ISPF面板和批处理组装HLASM的知识; 但是,您也可以使用as命令在USS中汇编这些程序。 有关此命令的更多信息,请参见《 z / OS USS命令参考 》。

代码位置

与C一样,您的HLASM代码可以驻留在本机z / OS数据集中,也可以驻留在USS文件中。 就像C一样,您可以在USS的PDS中汇编代码,从批处理中汇编USS文件,或两者的某种组合。

从HLASM访问USS功能

像前面提到的C程序一样,HLASM程序无需任何准备即可从本机z / OS和USS环境访问USS服务。 只需致电服务即可。 HLASM程序使用《 IBM z / OS Unix系统服务编程:汇编器可调用服务参考》中记录的汇编器可调用服务访问这些服务。 清单5显示了一些获取当前进程ID的代码,并将其放入全字PROCESSID字段中。 该代码从z / OS CSRTABLE控制块获取getpid()服务的地址。 《 z / OS USS编程:汇编器可调用服务参考》中记录了此控制块中每个服务的偏移量。

清单5.访问USS函数的HLASM代码
L     R15,16                  R15 -> Common Vector Table        
         L     R15,CVTCSRT-CVT(15)     R15 -> CSRTABLE                         
         L     R15,24(R15)             R15 -> CSR slot                         
         L     R15,276(R15)            R15 = Address of getpid svc    
         CALL  (15),(PROCESSID),VL

如果您不想遍历控制块来获取USS服务的地址,则有两种选择:

  • 使用所需的服务对模块执行z / OS LOAD(上例中为BPX1GID)
  • 链接到SYS1.CSSLIB中的链接存根。 在这种情况下, 清单5中的代码将更改为清单6中所示的代码。 在此示例中,BPX1GID将在链接编辑时指向SYS1.CSSLIB中的存根,该存根将分支到相关服务。
清单6.使用CALL访问USS函数的HLASM代码
CALL  BPX1GID,(PROCESSID),VL

您可以在《 IBM z / OS UNIX系统服务编程:汇编程序可调用服务参考》中找到有关HLASM的所有可用USS服务的文档。 本机z / OS还为SYS1.MACLIB中的USS区域提供了映射DSECTS。

从HLASM访问USS数据集

从HLASM访问USS数据集非常容易。 您有两种选择:

  1. 致电上述相关的USS汇编器可呼叫服务
  2. 使用本地z / OS BPAM宏,例如OPEN, BLDLCLOSE 。 有关更多信息,请参阅《 IBM z / OS DFSMS使用数据集》手册。

默认情况下,USS文件将信息存储在EBCDIC中(因此不需要ASCII;需要EBCDIC转换)。

从汇编器调用C程序

从HLASM(在USS和本机z / OS中)调用C程序与调用任何其他高级语言(HLL)程序相同,这基本上意味着您的HLASM程序必须是语言环境程序。 为此,您需要遵守用于寄存器和内存使用的语言环境标准,并使用一些z / OS提供的宏来启动和结束HLASM程序。 但是,让您感到困惑的是,哪些宏和标准(及其记录的位置)取决于寻址模式,并且您的程序是XPLINK还是非XPLINK:

  • 如果它是24位或31位且非XPLINK,请使用CEEENTRYCEETERM宏启动和终止程序。 有关更多信息,请参阅《 IBM z / OS语言环境编程指南》 。
  • 如果是31位和XPLINK,请使用EDCXPRLGEDCXEPLG宏启动和终止程序。 请参阅《 IBM z / OS C / C ++编程指南》 。
  • 如果它是64位的(这意味着它必须是XPLINK),请使用CELQPRLGCELQEPLG宏启动和终止程序。 请参阅《 IBM z / OS语言环境编程指南64位寻址模式》手册。

问题和提示

  • 企业COBOL和企业PL / I程序也可以在USS内编译和运行。 有关更多信息,请参见相应的编程指南。

绑定程序

在z / OS中,所有HLASM,C,PL / I和COBOL程序(无论它们在何处运行)都需要由z / OS Binder绑定(或链接编辑)。 绑定器可以在USS或本机z / OS中运行:

  • 在USS中:使用相同的c89函数来运行C / C ++编译器和z / OS绑定器。 您可以一次运行单独运行,也可以一起运行。 活页夹选项在c89的-W'L, options '标志中指定。
  • 在z / OS中:使用标准的ISPF面板,或提交批处理作业,这是本机z / OS程序员所熟悉的选项。

加载模块和对象位置

通常认为,模块必须驻留在它们要运行的位置,在USS的USS目录中,在本机z / OS的负载库/ PDSE中。 但这是不正确的。 对于USS应用程序,将同时搜索USS libpath(在环境变量中指定)和本机z / OS序列(作业包区域,STEPLIB DD,JOBLIB DD,LPA和Linklist,按此顺序)以查找被调用的模块。 首先搜索哪个取决于您的程序是使用POSIX运行时选项ON还是OFF运行,如果POSIX为ON,则首先运行libpath,如果POSIX为OFF,则首先运行本机z / OS库。

您可以使用C程序中的#pragma runopts(POSIX(ON))语句,JCL EXEC参数PARM='POSIX(ON)'或从语言环境运行时选项(使用_CEE_RUNOPTS环境变量进行设置)来设置此选项。

同样,本机z / OS应用程序可以驻留在USS文件中。 但是,这样做的问题是只能从在USS中运行的程序(以及指向该目录的USS loadhfs )或调用USS loadhfs服务的程序调用loadhfs

对象模块(已编译/组装但尚未绑定的模块)也可以驻留在PDS,PDSE或UNIX库中。 绑定程序时,可以在任何这些对象中引用对象。

调用程序-静态与动态

是静态调用另一个程序(被调用程序被链接编辑/绑定到同一个加载模块)还是动态调用另一个程序(被调用程序是在运行时加载的一个单独的模块)的问题是大多数程序员之前都会遇到的问题。 。

静态链接

要将模块静态链接到程序模块,您可以使用以下几种方法:

  • 向活页夹SYSLIN DD中添加INCLUDE语句以手动包含模块(仅适用于本地z / OS)。 例如:

    INCLUDE SYSLIB(INCMOD)

  • 指定活页夹CALL选项,并使模块在活页夹步骤的SYSLIB DD中可用(仅适用于本地z / OS)。
  • 在USS c89命令中包含模块/对象; 例如,在incmod.o中包含pgm1:

    c89 -o pgm1 incmod.o

  • 使用所有目标文件创建一个USS存档文件,并将其包含在c89命令中; 例如:

    c89 -o pgm1 incarch.a

  • 使用所有目标文件创建一个USS存档文件,并在绑定器SYSLIN DD中使用AUTOCALL语句(仅适用于本地z / OS); 例如:

    AUTOCALL /u/mydir/incmod.a

如果您有一个C程序调用一个非LE HLASM程序,或者一个非DLL程序直接调用DLL模块,则静态链接也是唯一的选择。

动态链接

C只能动态链接称为DLL (动态链接库)的新加载模块类型。 DLL与传统的z / OS加载模块不同。 他们

  • 必须驻留在PDSE或USS文件中。
  • 总是可重入的。
  • 的名称长度不能超过8个字符。
  • 可以是COBOL,PL / I,LE HLASM或C模块。 但是,在编译COBOL,PL / I或C时必须使用DLL选项,而在组装HLASM时必须使用GOFF和RENT选项。
  • 可以由COBOL和PL / I静态调用。
  • 可以使用FETCH通过PL / I程序动态链接。
  • 可以由COBOL程序使用CALL动态链接,但前提是必须将COBOL程序编译为DLL。
  • 是C程序可以动态调用另一个程序的唯一方法。

要创建DLL,您需要:

  • 如果在本机z / OS中绑定,请指定DYNAM=DLL Binder选项。
  • 如果在USS中调用绑定程序,则指定选项-W 'l,dll'
  • 即使模块仅包含函数,也请确保代码中包含main()语句。
  • 如果在本机z / OS中进行绑定,则包括语言环境端文件 CELHS001和CELHS003(如果为31位,则为CELQS003,如果为64位)。 清单9是一些示例JCL,显示了如何执行此操作。

《 IBM z / OS C / C ++用户指南》是从DLL开始的最佳位置。 上面的最后一点是关于边文件的 。 辅助文件是由绑定器在绑定DLL时自动创建的文件(尽管您也可以手动创建它)。 从批处理开始,将辅助文件发送到SYSDEFSD DD; 在USS中,活页夹创建一个以'x'结尾的文件(例如pgm1.x )。 该副文件实际上是一个包含活页夹指令的文件,该活页夹指令告诉它需要调用函数的模块。 对于具有两个功能的模块,它可能类似于清单7。

清单7.用于31位应用程序的z / OS绑定器的辅助文件输出
IMPORT CODE,'MODULE','My_First_Function'
      IMPORT CODE,'MODULE','My_Second_Function'

对于64位模块,相同的辅助文件应类似于清单8。

清单8.用于64位应用程序的z / OS绑定器的副文件输出
IMPORT CODE64,'MODULE','My_First_Function'
      IMPORT CODE64,'MODULE','My_Second_Function'

绑定动态调用DLL的程序时,您需要:

  • 如果以本机z / OS批处理方式运行,则在SYSLIN DD输入中将附带文件说明包括在活页夹中。
  • 如果在USS中运行,则将辅助文件添加到c89绑定指令中; 例如:

    c89 -o pgm1 dllfile1.x

否则,在绑定程序时,您将获得无法解析的外部引用。

HLASM程序员将熟悉LOAD宏,用于将模块加载到准备分支的存储中。 但是,这仅适用于本机z / OS模块。 要从USS目录加载模块,您需要loadhfs USS服务。

提示和技巧

  • 也可以使用USS extattr命令对APF文件中的模块进行APF授权。 例如:

    extattr +a mypgm

  • 在USS之外使用活页夹时,请记住使用CASE=MIXED活页夹选项; 否则,您的所有程序名称都将大写。
  • 如果在本机z / OS和USS之间移动DLL ,则需要使用z / OS Binder。 清单9清单10清单11显示了示例JCL,用于在USS和本机z / OS PDSE之间移动模块。 请注意, 清单9清单10中的ENTRY CEESTART语句将是用于64位模块的ENTRY CELQSTRT

    SCEELIB成员CELHS003和CELHS001是在本机z / OS上创建DLL时必需的语言环境端文件。

    还可以使用OGETXOPUTX TSO / E命令以及mvcp USS命令来移动非DLL模块。

  • 从z / OS 1.9起,辅助文件可以包含在USS归档文件中。
  • 无需在USS中运行绑定程序即可在USS中创建加载模块。 您可以在SYSLMOD DD的PATH DD语句中通过指定输出目录的批处理方式在USS中创建模块。 但是,从USS调用绑定程序时,很难创建本机z / OS加载模块。
  • 构建DLL时 ,可以导出要由另一个程序使用的函数/变量。 您可以在C代码中使用#pragma export语句或通过指定EXPORTALL C编译器选项来执行此操作。
清单9.将DLL从USS移到PDSE的JCL
//LINK    EXEC PGM=IEWBLINK,                            
// PARM='CALL,MAP,LET,LIST,COMPAT=PM4,DYNAM(DLL)'  
//SYSPRINT DD  SYSOUT=*                                 
//SYSLMOD  DD  DISP=SHR,DSN=MYHLQ.LOADLIB         
//SYSDEFSD   DD DUMMY
//INPUT    DD  PATH='/u/mydir/pgmname',          
//             PATHDISP=(KEEP,KEEP),PATHOPTS=(ORDONLY)  
//SYSLIB   DD  DSNAME=CEE.SCEEBND2,DISP=SHR             
//SYSLIN   DD  DSNAME=CEE.SCEELIB(CELHS003),DISP=SHR    
//         DD  DSNAME=CEE.SCEELIB(CELHS001),DISP=SHR    
//         DD  *                                        
 INCLUDE INPUT
 ENTRY CEESTART                                       
 NAME PGMNAME(R)
清单10.将非DLL模块从USS移至PDSE的JCL
//LINK    EXEC PGM=IEWBLINK,                            
// PARM='CALL,MAP,LET,LIST'  
//SYSPRINT DD  SYSOUT=*                                 
//SYSLMOD  DD  DISP=SHR,DSN=MYHLQ.LOADLIB         
//INPUT    DD  PATH='/u/mydir/pgmname',          
//             PATHDISP=(KEEP,KEEP),PATHOPTS=(ORDONLY)  
//SYSLIN   DD  *                                        
 INCLUDE INPUT
 ENTRY CEESTART                                       
 NAME PGMNAME(R)
清单11.将模块从PDSE移至USS的JCL
//LINK    EXEC PGM=IEWBLINK,
// PARM='CALL,MAP,LET,LIST,COMPAT=CURRENT,DYNAM(DLL)'
//SYSPRINT DD  SYSOUT=*
//SYSDEFSD DD  DUMMY
//INPUT    DD  DISP=SHR,DSN=MYHLQ.LOADLIB
//SYSLIB   DD  DSNAME=CEE.SCEEBND2,DISP=SHR
//SYSLMOD  DD  PATH='/u/mydir/pgmname',
//             PATHDISP=(KEEP,KEEP),PATHOPTS=(ORDWR,OCREAT,OTRUNC),
//             PATHMODE=(SIRWXU,SIRWXG,SIRWXO)

最后的话

跨过本机z / OS和USS边界的边界非常容易。 实际上,根本没有边界。 z / OS是一个具有两个不同接口的操作系统。 因此,除了正常的问题和打h之外,您可能会发现访问各种服务的最大问题将是习惯于USS和本机z / OS之间的词汇差异。


翻译自: https://www.ibm.com/developerworks/aix/library/au-bordercrossing/index.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值