Linux java jni调用C++封装动态库

Java通过JNA或JNI调用C++动态库 阅读详情

        由于项目中java需要调用第三方提供的C++动态库;由于第三方动态库传入的参数较多,还伴随着指针传入操作,导致java调用极为不便!因此催生出对于第三方的C++动态库进行二次封装。java调用只需按结构传入一个结构化的string即可。话不多说开干!

第三方库

        第三方C++的头文件HD10_safe.h如下:

#ifndef _HD10_safe_H_
#define _HD10_safe_H_

#ifdef _cplusplus
extern "C" {
#endif

typedef	enum VKeyGenResultEx
{
	KGRE_OK 	,
  	KGRE_NOT_OK
}KeyGenResult;


typedef enum ECUS
{
	BCM=1, DCM, IC_6000, GW, VWM, LDWS, DMS, VCU, RCU, ACC, 
	RWCD, PSU, BBM, AC_6000, TPMS, VMS, PEPS, IMMO, EMS, ESCL,
	MMI, BMS, FC, EC, ECM, CPD_3000, AFS, TCO, TCU, DCU,
	ECAS, ABS, EBS, EPB, AEBS, EVM, TCU_SH, ADAS, BS_LKA, CIM,
	RIM, FIM, MCU, CABS, DSW, VCU_x5_M3S, SWG, CCU, WP_ECU, CMS_ECU,
	EHBS, ACR, TBOX_5G, CSC, HCU, LKA, ADCU, CPD_6000, IC_3000, AC_3000,
	LE_IC, LE_AC, LE_BCM, LE_GW, LE_VCU, LE_TPMS, LE_DCM, VDCU, PDU, DCDC, DCAC,         DSSAD
}ECUs;

typedef unsigned char	uint8; 
typedef unsigned short	uint16;       
// Function
//
KeyGenResult HD10GenerateKeyEx(uint8*       iSeedArray,	/* Array for the seed [in] */
                          uint8			iSeedArraySize,	/* Length of the array for the seed [in] */
                          const uint8	iSecurityLevel,	/* Security level [in] */
                          uint8 *		iKeyArray,	/* Array for the key [in, out] */
                          uint8 *		iKeySize,	/* Length of the key [out] */
						  ECUs			e			/* enum ECUS elements */
                          );

#ifdef _cplusplus
}
#endif

#endif // _HD10_safe_H_

        第三方动态库 libTXJsafe.so 如下:

C++调用

        写一个C++ demo 调用动态库 test.cpp 代码如下:

#include <stdio.h>
#include "HD10_safe.h"

int main()
{
	uint8 arr[2] = {0x12, 0x34};
	uint8 seed[2] = {0x00, 0x00};
	uint8 seedLen = 0;
	
	int ret = HD10GenerateKeyEx(arr, 2, 7, seed, &seedLen, TCU);
	
	printf("ret : %d, seedLen:%d \n", ret, seedLen);
	
	for(int i = 0; i < seedLen; i++)
	{
		printf("seed[%d] : 0x%02x ;", i, seed[i]);
	}
	printf("\n");
	
	return 0;
}

        运行结果如下:

jni二次封装动态库

        通过jni二次封装动态库 mysafelib.cpp 代码如下:

#include <iostream>
#include <stdio.h>
#include <jni.h>
#include <string.h>
#include "HD10_safe.h"

// iString 输入参数格式:level(1byte) + ECUS(1byte) + SeedSize(1byte) + SeedValue(nbyte)
// rString 返回参数格式:keysize(1byte) + keyValue(nbyte)
extern "C" {
    JNIEXPORT jstring JNICALL Java_com_example_MyClass_nativeMethod(JNIEnv *env, jobject obj, jstring iString);
}

// 十六进制字符串转字十六进制 "12" --> 0x12
int Str2Hex(char *p_hexstr, int iHexLen, char *pdststr);

// 十六进制数值转十六进制字符串 0x12 --> "12"
int Hex2Str(const char *p_strstr, int iStrLen, char *pdststr);

JNIEXPORT jstring JNICALL Java_com_example_MyClass_nativeMethod(JNIEnv *env, jobject obj, jstring iString)
{
	char myRString[66] = {0};
	char iHexStrOrg[64] = {0};
	char iHexStr[32] = {0};
	
	const char *iStrData = env->GetStringUTFChars(iString, 0);
	memcpy(iHexStrOrg, iStrData, strlen(iStrData) > 64 ? 64 : strlen(iStrData));
	env->ReleaseStringUTFChars(iString, iStrData);
	
	int iHexStrLen = Str2Hex(iHexStrOrg, strlen(iHexStrOrg), iHexStr);
	
	// std::cout << "strlen(iHexStrOrg) : " << strlen(iHexStrOrg) << std::endl;
	
	if(iHexStrLen < 4)	return env->NewStringUTF(myRString);
	
	// std::cout << "iHexStrLen : " << iHexStrLen << std::endl;
	
	uint8 seed[32] = {0};
	uint8 key[33] = {0};
	uint8 keyLen = 0;
	
	uint8 u8Level		= *(uint8 *)iHexStr;
	uint8 u8ECU	 		= *(uint8 *)(iHexStr + 1);
	uint8 u8SeedSize	= *(uint8 *)(iHexStr + 2);
	
	// printf("u8SeedSize : %d \n", u8SeedSize);
	
	if(u8SeedSize > 32)	return env->NewStringUTF(myRString);

	memcpy(seed, iHexStr + 3, u8SeedSize);
	
	KeyGenResult ret = (KeyGenResult)HD10GenerateKeyEx(seed, u8SeedSize, u8Level, key + 1, &keyLen, (ECUs)u8ECU);
	
	if(ret != KGRE_OK || keyLen > 32)	return env->NewStringUTF(myRString);
	
	// printf("ret : %d, keyLen:%d \n", (int)ret, keyLen);
	
	// for(int i = 1; i <= keyLen; i++)	printf("key[%d] : %02x; ", i, key[i]);
	
	key[0] = keyLen * 2;
	
	Hex2Str((const char*)key, keyLen + 1, myRString);

	jstring rString = env->NewStringUTF(myRString);
	
	printf("\n---------------successful-----------------\n");
	
    return rString;
}

// 十六进制字符串转字十六进制 "12" --> 0x12
int Str2Hex(char *p_hexstr, int iHexLen, char *pdststr)
{
	int iret = 0;
	while(p_hexstr != NULL && pdststr != NULL && iHexLen > 1)
	{
		char cTemp = '0';
		
		// printf("1:%c, 2:%c\n", p_hexstr[0], p_hexstr[1]);
		
		// 小写统一转大写
		if(p_hexstr[0] >= '0' && p_hexstr[0] <= '9')
		{
			cTemp = p_hexstr[0] - '0';
		}
		else if(p_hexstr[0] >= 'A' && p_hexstr[0] <= 'F')
		{
			cTemp = p_hexstr[0] - 'A' + 10;
		}
		else if(p_hexstr[0] >= 'a' && p_hexstr[0] <= 'f')
		{
			cTemp = p_hexstr[0] - 'a' + 10;
		}
		else
		{
			printf("the hex str is error!\n");
			break;
		}
		
		*pdststr = cTemp * 16;
		
		if(p_hexstr[1] >= '0' && p_hexstr[1] <= '9')
		{
			cTemp = p_hexstr[1] - '0';
		}
		else if(p_hexstr[1] >= 'A' && p_hexstr[1] <= 'F')
		{
			cTemp = p_hexstr[1] - 'A' + 10;
		}
		else if(p_hexstr[1] >= 'a' && p_hexstr[1] <= 'f')
		{
			cTemp = p_hexstr[1] - 'a' + 10;
		}
		else
		{
			printf("the hex str is error!\n");
			break;
		}
		*pdststr += cTemp;
		
		// printf("---iHexLen:%d, pdststr:%c\n", iHexLen, *pdststr);
		
		iHexLen -= 2;
		p_hexstr += 2;
		pdststr++;
		iret++;
	}
	
	// printf("iret : %d\n", iret);
	
	return iret;
}
 
// 十六进制数值转十六进制字符串 0x12 --> "12"
int Hex2Str(const char *p_strstr, int iStrLen, char *pdststr)
{
	int index_str = 0, index_hex = 0;
 
	const char cHex[] = "0123456789ABCDEF";
	while(index_str < iStrLen && p_strstr != NULL && pdststr != NULL)
	{
		pdststr[index_hex++] = cHex[((uint8)p_strstr[index_str])/16];
		pdststr[index_hex++] = cHex[((uint8)p_strstr[index_str++])%16];
	}
 
	return index_hex;
}

        编译生成对应二次封装的动态库如下:

java调用二次封装动态库

        java调用二次封装库的demo文件如下:

package com.example;

public class MyClass {
    static {
        System.load("/home/lijd/testlib3/sodir/libmysafe.so"); // 加载C++动态库
    }

    private native String nativeMethod(String str); // 声明本地方法

    public static void main(String[] args) {
        byte[] seedValue = new byte[]{0x12, 0x34};
        int seedSize = 2;//字节长度
        int level = 7;
        int ECUS = 29;

        StringBuffer param = new StringBuffer();
        param.append(byteToString(intToByte1(level)));
        param.append(byteToString(intToByte1(ECUS)));
        param.append(byteToString(intToByte1(seedSize)));
        param.append(byteToString(seedValue));
        String result = new MyClass().nativeMethod(param.toString()); // 调用本地方法
		System.out.println("---MyClass---" + result.length() + " : " + result);
    }


    /**
     * @title byteToString
     * @description 将字节数组转为字符串
     * @param buff
     *            字节数据
     * @return String 装换后的字符串数据
     */
    public static final String byteToString(byte[] buff) {
        StringBuilder sb = new StringBuilder();
        if (null != buff && buff.length > 0) {
            for (byte b : buff) {
                short t = b;
                if (t < 0)
                    t += 256;
                short h = (short) (t / 16);
                short l = (short) (t % 16);

                switch (h) {
                    case 10:
                        sb.append("A");
                        break;
                    case 11:
                        sb.append("B");
                        break;
                    case 12:
                        sb.append("C");
                        break;
                    case 13:
                        sb.append("D");
                        break;
                    case 14:
                        sb.append("E");
                        break;
                    case 15:
                        sb.append("F");
                        break;
                    default:
                        sb.append(h);
                        break;
                }

                switch (l) {
                    case 10:
                        sb.append("A");
                        break;
                    case 11:
                        sb.append("B");
                        break;
                    case 12:
                        sb.append("C");
                        break;
                    case 13:
                        sb.append("D");
                        break;
                    case 14:
                        sb.append("E");
                        break;
                    case 15:
                        sb.append("F");
                        break;
                    default:
                        sb.append(l);
                        break;
                }

            }
        }

        return sb.toString();
    }

    public static byte[] intToByte1(int value) {
        byte[] b = new byte[1];

        for (int i = 0; i < 1; ++i) {
            int offset = (b.length - 1 - i) * 8;
            b[i] = (byte) (value >>> offset & 255);
        }
        return b;
    }
}

        执行javac 编译源文件生产class文件如下:

        运行java的class文件如下:

        至此,一切搞定!通过windows测试这样也完全可以。

使用Java在Linux上调用动态链接库的JNI编程 首先,我们创建了一个包含本地函数的C文件,并将其编译为动态链接库。然后,在Java类中加载该动态链接库,并声明本地方法来调用其中的函数。在Linux环境下,Java提供了JNI(Java Native Interface)机制,允许开发者使用Java调用本地(C/C++)动态链接库。通过以上步骤,我们成功在Linux环境下使用Java JNI调用了动态链接库中的本地函数。接下来,创建一个Java类,用于加载动态链接库并调用其中的本地函数。方法加载编译好的动态链接库,并声明一个本地方法来调用其中的函数。 阅读详情

相关推荐

深入学习Java中调用C/C++的过程及编程实例

通过JNI,我们可以在Java代码中声明和调用本地方法,这些本地方法实际上是由C/C++代码实现的。通过Java调用C/C++代码,我们可以利用C/C++的底层能力,实现高性能的计算、底层操作和与现有C/C++库的交互。然而,使用本地方法需要谨慎,确保代码的正确性和安全性,避免出现潜在的内存泄漏和崩溃等问题。在Java编程中,有时需要调用底层的C或C++代码来实现一些高性能、底层操作或与现有C/C++库进行交互的功能。这种方法可以扩展Java的功能,提供更高性能的计算和与现有C/C++库的交互能力。

TechBurst的博客 387

java调用c++的几种方式

类似c#调用c++的方式,定义java端的c++代码接口。

daoer_sofu的专栏 3862

手把手教你用芯驰X9HP驱动MAX96755/MAX96752点亮智能座舱仪表屏(附完整寄存器配置表)

本文详细介绍了如何使用芯驰X9HP驱动MAX96755/MAX96752实现智能座舱仪表屏的完整开发流程,包括硬件架构解析、关键寄存器配置、Linux BSP集成及调试技巧。通过实战指南和完整寄存器配置表,帮助开发者快速掌握驱动开发核心技术,提升智能座舱系统的显示性能与稳定性。

weixin_30879169的博客 525

【教程】如何用Java调用C++代码?

简单讲解如何用Java调用C++代码输出HelloWorld,包含传参和不传参两种实现方式。

qq_63211416的博客 5658

Plant simulation、Flexsim、Automod、Emulate3D、VisuaComponent仿真软件对比

E3D在行业内以展示效果好,入门阶段易学易用人机界面友好,电控仿真连接方便不需要借助其他第三方软件,E3D平台开放度较高用户DIY程度强,数据分析功能针对要评定对象的KPI的可根据项目或者使用者需求进行评定。3.支持导入导出多种格式的文件,比如dwg、dxf、fbx、obj、3ds、step、stp、iges、skp、stl、wrl、 bmp 等等。3.软件自带模型组件库程序代码开放,用户可以引用、修改、拷贝,按照自己的逻辑修改模型,或者创建模型并保存到组件库与别人共享。3. 支持导出布局图片。

qq_43342303的博客 3576

PaddleOCR实战:从零开始打造专属OCR模型的完整指南

本文提供了一份从零开始使用PaddleOCR打造专属OCR模型的完整实战指南。内容涵盖环境搭建、数据标注与准备、模型训练与调优,以及最终的模型转换与部署。重点介绍了如何利用PaddleOCR进行模型训练,并整合PPOCRLabel等工具高效准备数据集,帮助开发者快速构建适用于特定场景的高精度OCR识别能力。

rl6adventurer的博客 733

LINUX下JNI封装 C++动态链接库

这几天用jni封装了一个C++库文件,供java调用,在此把具体的步骤给理一下,方便自己温故,也供大家参考。 前提:已有一个动态库文件libTest.so和包含函数声明的头文件test.h。 步骤一:先创建一个java项目:JProj,添加一个包:com,在包下创建两个类:JDefine.java,JTest.java,其中JDefine.java用来创建对象和申请函数。如头文件test.h:

知行合一的博客 6261

波士顿咨询最新发布《2025年全球支付报告:未来不稳定》:到2029 年,支付收入将预计达到 2.4 万亿美元

波士顿咨询最新发布《2025年全球支付报告:未来不稳定》:到2029 年,支付收入将预计达到 2.4 万亿美元

face light~ 917

java jni实现jieba c++库的java调用

网上一直没有java调用c++的jieba分词,最近自己项目需求,做了一下简单的封装,经过本人实际测试可用,中间会有差别,欢迎留言,我会尽量解答。 1,编写java代码,java代码如下 import java.util.Arrays; import java.util.Map; public class JiebaForJava { /** * cut word ...

lx17671694697的博客 558

EC20模块实战:5步搞定MQTT连接阿里云IoT(附常见错误排查)

本文详细介绍了使用EC20 4G模块通过AT指令连接阿里云IoT平台的完整实战流程。文章以智能农业项目为背景,从硬件接线、网络注册、PDP激活,到MQTT连接配置、一型一密动态注册及数据收发,提供了清晰的5步操作指南,并重点附上了常见错误排查方法,帮助开发者快速实现稳定可靠的物联网设备上云。

weixin_29209801的博客 196

Verilog-asm小练习—PLL建模

Verilog- ams是Verilog标准的模拟混合信号语言,以下是对PLL建模的一种思路参考

weixin_44700185的博客 1016

【2015-04-27】Linux下使用JNI封装调用已经存在的动态静态库

介绍在Linux下使用JNI封装调用已有的C动态库或静态库,通过编写JNI中间层(C wrapper)将第三方库的函数桥接给Java调用,涵盖javah生成头文件、C封装编译为新.so库以及Java端System.loadLibrary加载的完整流程。

cstriker1407的专栏 177

华为OD机考双机位C卷 - 员工派遣 (Java & Python& JS & C/C++ & GO )

华为OD机考双机位C卷 - 员工派遣 (OD上机考试双机位C卷)。提供在线OJ刷题。Java、C++、Python、GO语言、C语言、JavaScript题解,并提供完整思路讲解。

算法大师! 1032

C++小白,关于java调用C++接口,使用swig封装教程

一、Windows版本----测试机win10 1.swig的下载地址: http://www.swig.org/download.html win10 配套版本号是 swigwin-4.0.2 2.设置swig环境变量 D:\swigwin-4.0.2 设置以后使用swig -help命令测试是否安装成功 编写.i文件: %module libngesTdApi %{ %include ".\Ft...

Amyliaa的博客 550

基于逆向工程的中国裁判文书网爬虫技术深度解析:从反爬破解到合规抓取

最近承接了一个法律大数据分析的项目,需要获取某类合同纠纷的裁判文书数据,用于分析不同地区的判决规律、法官的裁判倾向以及法条适用情况。原本以为用常规的Python爬虫就能搞定,结果刚对着中国裁判文书网发了几个请求,就遭遇了“滑铁卢”:要么请求直接返回403 Forbidden,要么触发滑块验证码,甚至连IP都被临时封禁了。普通的requests+BeautifulSoup组合在裁判文书网面前完全失效,这也让我意识到,想要获取这里的数据,必须先通过逆向工程拆解网站的请求逻辑和反爬机制。经过一周的摸索,从抓包分析

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 3168

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Java调用动态库(dll)最简便方法和最好用的组件

essageBoxA,具体步骤是 Java 中声明一个 native 方法,然后用 javah 命令生成JNI样式的头文件,再自己实现头文件中声明的方法,在实现方法中装载动态库 user32.dll,调用 MessageBoxA 方法,需要把自己写的这部分 C/C++ 代码封装成一个动态库,如Sample.dll,最后在 java 中装载 Sample.dll,然后执行其中所声明的本地方法。 可见,用老实的JNI方式,我们在调用一个已知动态库的时候还需要额外生成一个符合JNI规则的动态库作为桥梁,显

{丸の子} 242

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Linux环境下使用java jni调用C、C++程序

Linux环境下使用java jni调用C、C++程序 前言 由于工作需要,必须使用FPGA硬件加速算法,但是算法又是使用C和C++实现的,而项目是web项目,是通过Java实现的,这就涉及到java调C\C++。下面是本人测试Java调C(C和C++的流程差不多,下面统称C)的流程,采用的技术是Java jni(Java本地接口)。 实现流程 首先我们需要编写一个简单的demo用来调用C程序。...

qq_40874400的博客 794

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

Linux下JAVA调用C++动态库

公司项目要求对接腾讯云的实时音视频(Tencent Real-Time Communication,TRTC),其开发语言是C++,需封装成java接口。以前没有做过类似的项目,边踩坑边学习,目前思路基本捋顺,记录一下备忘。 腾讯提供了以下文件: *libTRTCEngine.so* C++业务实现底层核心代码编译而成的动态库文件。 *recordsdk.i* swig接口文件,c++转jav...

菜鸟日记 2188

Linux C++ JNI封装、打包成jar包供Java调用详细介绍

在前面中详细介绍了如何在Android Studio中调用通过jni封装的c++库。在Android使用 opencv c++代码,需要准备opencv4android,也就是c++的任何代码,是使用Android NDK编译的,相当于在windows/mac上使用Android stdido交叉编译。本文再介绍服务端的使用方式,c++通过jni封装的库,直接被java后端服务代码调用。这里c++依赖库都是linux主机上,jni有关库也都是linux上的,因此就不存在交叉编译。

热爱生活,忠于自己 1664

Linux下java使用jni调用c++程序

文章目录一.安装g++编译环境1.查看g++版本2.安装g++3.g++基础指令参数说明二.Java使用JNI调用C++1.写一个HelloWorld的java程序2.编译HelloWorld.java3.生成c程序头文件5.生成动态库.so文件6.将当前文件路径添加到共享库中7.运行java程序![在这里插入图片描述](https://img-blog.csdnimg.cn/bfb9543f47d440219b6215cd57600b54.png)三.linux指令界面 一.安装g++编译环境 1.查看g

m0_50157558的博客 746

linux下 使用JNI 来以C++调用JAVA的类!(一)

           决定使用JNI,实际是为了能够将通讯与调用后台的lucene索引,因此老大决定要采用这种方式来实现index的多机分布式的索引服务。接到任务,使用C++来调用Lucene的java查询的封装类。        用了java,c++各一段时间,却从未接触过JNI. 开始从网上收集该方面的资料,从头开始没有指导的时候特别郁闷,网上找到的大多数资料是讲述如何用java来调用C+

doomhuntercj的专栏 5113

【Linux】下跨语言调用【C++】实践

【查询理解】(QU, Query Understanding)是美团搜索的核心模块,主要职责是理解用户查询,生成查询意图、成分、改写等基础信号,应用于搜索的召回、排序、展示等多个环节,对搜索基础体验至关重要。该服务的线上主体程序基于C++语言开发,服务中会加载大量的词表数据、预估模型等,这些数据与模型的离线生产过程有很多文本解析能力需要与线上服务保持一致,从而保证效果层面的一致性,如文本归一化、分词等。而这些离线生产过程通常用Python与Java实现。如果在线、离线用不同语言各自开发一份,则很难维持策略与

choice~的博客 2587

java 封装动态库_Java调用动态库(转载)

记得以前 Java 要调用C/C++写得动态库都是用的 JNI 方式,还需要自己写不少 C/C++ 代码。比如说要在Java中调用已有的动态库,如 Windows 的 user32.dll 的方法 MessageBoxA,具体步骤是 Java 中声明一个 native 方法,然后用 javah 命令生成JNI样式的头文件,再自己实现头文件中声明的方法,在实现方法中装载动态库 user32.dll,...

weixin_34392227的博客 453

网盘直链下载工具指南:LinkSwift 一个脚本拿到九大网盘真实下载地址

登录百度网盘,勾选要保存的文件,点一下页面角落的「下载助手」,LinkSwift 几秒内就生成该文件的真实下载地址,可以直接推给 IDM、Aria2、cURL 等下载工具。这是一款开源的网盘直链下载 Tampermonkey 油猴脚本,下文从安装到进阶把用法一次讲清。 ## 📍 项目定位:它是什么、适合谁 LinkSwift 是一个基于 JavaScript 油猴脚本的**网盘文件下载地址获

gitblog_00415的博客 768

linux下通过JNI用C/C++中调用JAVA类

决定使用JNI,实际是为了能够将通讯与调用后台的lucene索引,因此老大决定要采用这种方式来实现index的多机分布式的索引服务。接到任务,使用C++来调用Lucene的java查询的封装类。         用了java,c++各一段时间,却从未接触过JNI. 开始从网上收集该方面的资料,从头开始没有指导的时候特别郁闷,网上找到的大多数资料是讲述如何用java来调用C++的,而且多试wi

qq_14898543的博客 3600
上一篇: C++11 shared_from_this学习
下一篇: 平衡二叉搜索树AVL(C实现)
码农诗人
博客等级 码龄11年 986粉丝 84原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值