djinni使用实践(一) -- 官方文档

7 篇文章 0 订阅
7 篇文章 0 订阅

djinni使用实践(一) -- 官方文档

djinni使用实践(二) -- djinni究竟都有哪些配置选项

djinni使用实践(三)--Android项目中的使用

djinni项目地址

Djinni


Djinni 是一个生成跨语言类型声明和接口绑定的工具。 它旨在将 C++ 与 Java 或 Objective-C 连接起来。 Python 支持在 python 分支上的实验版本中可用。

Djinni 可用于在 Android 和 iOS 上将跨平台 C++ 库代码与特定于平台的 Java 和 Objective-C 接口。 我们在 CppCon 2014 上发布了 Djinni。你可以看到幻灯片和视频。 有关 Djinni 以及其他人如何使用它的更多信息,请查看本文档末尾的社区链接。

维护说明:此 repo 是稳定的,但不再由 Dropbox 维护。 如果您有任何疑问或想与 Djinni 的其他用户交谈,您可以通过本文档末尾的链接加入 Slack 社区。

主要特点


  • 从单个接口描述文件生成并行 C++、Java 和 Objective-C 类型定义。
  • 支持三种核心语言的原始类型和用户定义的枚举、记录和接口的交集。
  • 生成接口代码,允许在 C++ 和 Java(使用 JNI)或 Objective-C(使用 Objective-C++)之间进行双向调用。
  • 可以自动生成数据类型的比较器函数(相等、排序)

入门


类型

Djinni 根据 IDL 文件中的接口定义生成代码。 IDL 文件可以包含三种声明:枚举、记录和接口。

  • Enums成为 C++ enum类、Java enums或 ObjC NS_ENUM。
  • Flags成为带有方便的面向位运算符的 C++ 枚举类、带有 EnumSet 的 Java 枚举或 ObjC NS_OPTIONS。
  • Records是纯数据值对象。
  • Interfaces是具有要调用的定义方法的对象(在 C++ 中,由 shared_ptr 传递)。 Djinni 生成的代码允许从 ObjC 或 Java 透明地使用在 C++ 中实现的接口,反之亦然。

IDL 文件


Djinni 的输入是一个接口描述文件。 下面是一个例子:

# Multi-line comments can be added here. This comment will be propagated
# to each generated definition.
#枚举
my_enum = enum {
    option1;
    option2;
    option3;
}

#标记
my_flags = flags {
  flag1;
  flag2;
  flag3;
  no_flags = none;
  all_flags = all;
}

#数据类
my_record = record {
    id: i32;
    info: string;
    store: set<string>;
    hash: map<string, i32>;

    values: list<another_record>;

    # Comments can also be put here

    # Constants can be included
    const string_const: string = "Constants can be put here";
    const min_value: another_record = {
        key1 = 0,
        key2 = ""
    };
}

another_record = record {
    key1: i32;
    key2: string;
} deriving (eq, ord)

# 定义一个接口,+c表示在c语言中实现这个接口
my_cpp_interface = interface +c {
    method_returning_nothing(value: i32);
    method_returning_some_type(key: string): another_record;
    static get_version(): i32;

    # 常量
    const version: i32 = 1;
}

# 定义一个接口,+j +o分别表示在java/ObjectC(ios)语言中实现这个接口
my_client_interface = interface +j +o {
    log_string(str: string): bool;
}

Djinni文件也可以互相包含。 添加行:

#路径为当前文件的相对路径
@import "relative/path/to/filename.djinni"

在文件开头,将仅包含另一个文件。 子文件路径是相对于包含@import的文件的位置而言的。 两个不同的djinni文件不能定义相同的类型。 @import的行为类似于在C ++中与#pragma一起#include的行为,或与ObjC的#import行为:如果通过不同的路径多次包含文件,则该文件只会被处理一次。

生成代码


当 Djinni 文件准备好后,您可以从命令行或 bash 脚本运行:

src/run \
   --java-out JAVA_OUTPUT_FOLDER \ #生成的java文件的输出路径(相对路径),可自定义
   --java-package com.example.jnigenpackage \ #生成的java文件的包名
   --java-cpp-exception DbxException \ # 在 Java 中的自定义 C++ 异常和 java.lang.RuntimeException(默认)之间进行选择。
   --ident-java-field mFooBar \ # 可选,这会在 Java 字段名称前添加一个“m”
   \
   --cpp-out CPP_OUTPUT_FOLDER \ #生成的C++文件的输出路径(相对路径)
   \
   --jni-out JNI_OUTPUT_FOLDER \ #生成的jni文件的输出路径(相对路径)
   --ident-jni-class NativeFooBar \ # 在生成的JNI类(类名)加“Native”前缀(当然也可以是JNIFooBar,随意)
   --ident-jni-file NativeFooBar \ # 这会为 JNI 文件名添加前缀,否则 cpp 和 jni 文件名相同
   \
   --objc-out OBJC_OUTPUT_FOLDER \ #生成的oc文件的输出路径(相对路径)
   --objc-type-prefix DB \ # Apple 建议 Objective-C 类为每个定义的类型都有一个前缀
   \
   --objcpp-out OBJC_OUTPUT_FOLDER \
   \
   --idl MY_PROJECT.djinni #你定义的djinni文件

其他一些选项也可用,例如 --cpp-namespace 将生成的 C++ 代码放入指定的命名空间。 要查看所有选项的列表,请运行

src/run --help

示例生成代码位于此发行版的 example/generated-src/ 和 test-suite/generated-src/ 目录中。

请注意,如果未指定语言的输出文件夹,则不会生成该语言。 有关更多信息,请运行 run --help 以查看所有可用的命令行参数。

在项目中使用生成的代码

Java / JNI / C++ 项目

Includes & Build target

TypeC++ headerC++ sourceJavaJNI headerJNI source
Enum/Flagsmy_enum.hppMyEnum.javaNativeMyEnum.hppNativeMyEnum.cpp
Recordmy_record[_base].hppmy_record[_base].cpp (+)MyRecord[Base].javaNativeMyRecord.hppNativeMyRecord.cpp
Interfacemy_interface.hppmy_interface.cpp (+)MyInterface.javaNativeMyInterface.hppNativeMyInterface.cpp

(+) 仅为包含常量的类型生成。 将所有生成的源文件添加到您的构建目标,以及 support-lib/java 的内容。

JNI 方法

JNI 代表 Java 本地接口,是 Java 语言的扩展,允许与本地 (C/C++) 代码或库进行互操作。 有关 JNI 的完整文档可从以下网址获得:Contents

对于每种类型,内置(列表、字符串等)或用户定义,Djinni 生成一个带有 toJava 和 fromJava 函数的翻译器类来来回翻译。

应用程序代码负责 JNI 库的初始加载。 在代码中的某处添加一个静态块:

System.loadLibrary("YourLibraryName");

如果您将本地库打包在 jar 中,您还可以使用 com.dropbox.djinni.NativeLibLoader 来帮助解包和加载您的库。 有关详细信息,请参阅本地主机自述文件

当调用本地库时,JNI 会调用一个名为 JNI_OnLoad 的特殊函数。 如果所有 JNI 接口代码都使用 Djinni,请包含 support_lib/jni/djinni_main.cpp; 如果没有,您需要添加对您自己的 JNI_OnLoad 和 JNI_OnUnload 函数的调用。 有关详细信息,请参阅 support-lib/jni/djinni_main.cpp

Objective-C / C++ 工程

Objective-C/C++生成的文件如下(假设前缀为DB):

TypeC++ headerC++ sourceObjective-C filesObjective-C++ files
Enum/Flagsmy_enum.hppDBMyEnum.h
Recordmy_record[_base].hppmy_record[_base].cpp (+)DBMyRecord[Base].hDBMyRecord[Base]+Private.h
DBMyRecord[Base].mm (++)DBMyRecord[Base]+Private.mm
Interfacemy_interface.hppmy_interface.cpp (+)DBMyInterface.hDBMyInterface+Private.h
DBMyInterface+Private.mm

(+) 仅为包含常量的类型生成。 (++) 仅为具有派生操作和/或常量的类型生成。 这些具有 .mm 扩展名以允许非平凡常量

将所有生成的文件添加到您的构建目标,以及 support-lib/objc 的内容。 请注意,+Private 文件只能与 ObjC++ 源(其他标头是纯 ObjC)一起使用,并且接口的 Objective-C 用户不需要。

生成类型的详细信息


Enum

枚举被转换为底层类型为 int 的 C++ 枚举类、底层类型为 NSInteger 的 ObjC NS_ENUM 和 Java 枚举。

Flags

为方便起见,Flags被转换为 C++ 枚举类,其底层类型为无符号和生成的一组重载按位运算符,底层类型为 NSUInteger 的 ObjC NS_OPTIONS 和 Java EnumSet<>。 与上述枚举相反,标志的枚举代表单个位而不是整数值。

在 IDL 文件中指定标志类型时,您可以为选项分配特殊语义:

my_flags = flags {
  flag1;
  flag2;
  flag3;
  no_flags = none;
  all_flags = all;
}

在上面的例子中,标有 none 和 all 的元素被赋予了特殊的含义。 在 C++ 和 ObjC 中,no_flags 选项是使用没有设置位(即 0)的值生成的,而 all_flags 是作为所有其他值的按位或组合生成的。 在 Java 中,不会生成这些特殊选项,因为可以只使用 EnumSet.noneOf() 和 EnumSet.allOf()。

Record

Record是数据对象。 在 C++ 中,Record按值包含其所有元素,包括其他记录(因此记录不能包含自身)。

数据类型

数据类、参数或返回值的可用数据类型有:

  • Boolean (bool)
  • Primitives (i8, i16, i32, i64, f32, f64).
  • Binary (binary). 这在 C++ 中实现为 std::vector<uint8_t>,在 Java 中实现为 byte[],在 Objective-C 中实现为 NSData。
  • Date (date). 这是 C++ 中的 chrono::system_clock::time_point、Java 中的 Date 和 Objective-C 中的 NSDate。
  • List (list<type>). 这是 C++ 中的 vector<T>、Java 中的 ArrayList 和 Objective-C 中的 NSArray。 列表中的原语将在 Java 和 Objective-C 中装箱。
  • Set (set<type>). 这是 C++ 中的 unordered_set<T>、Java 中的 HashSet 和 Objective-C 中的 NSSet。 集合中的原语将被装箱在 Java 和 Objective-C 中。
  • Map (map<typeA, typeB>). 这是 C++ 中的 unordered_map<K, V>、Java 中的 HashMap 和 Objective-C 中的 NSDictionary。 地图中的原语将在 Java 和 Objective-C 中装箱。
  • Enumerations / Flags
  • 可选(可选<typeA>)。 这是 C++11 中的 std::experimental::optional<T>,Java 中的对象/装箱原始引用(可以为 null),以及 Objective-C 中的对象/NSNumber 强引用(可以为 nil)。
  • 其他数据类型。 这是使用按值语义生成的,即复制方法将对内容进行深度复制。

扩展

为了支持额外的字段和/或方法,可以在任何语言中用“extended”扩展数据类。 要以一种语言扩展数据类,您可以在record标记后添加 +c (C++)、+j (Java) 或 +o (ObjC) 标志。 生成的类型将有一个 Base 后缀,您应该创建一个没有扩展数据类型后缀的派生类型。

派生类型必须以与 Base 类型相同的方式构造。 接口将始终使用派生类型。

派生方法

对于record类型,支持 Haskell 风格的“派生”声明来生成一些常用方法。 Djinni 能够生成相等和顺序比较器,在 C++ 中实现为运算符重载,在 Java/Objective-C 中实现为标准比较函数。

注意事项:

  • record中的所有字段都按照它们在记录声明中出现的顺序进行比较。 如果以后需要添加字段,请确保顺序正确。
  • 集合类型、选项和布尔值不支持排序比较。
  • 要比较包含其他record的record,内部record必须至少派生出与外部record相同类型的比较器。

Interface

仅适用于 C++ 的特殊方法

+c 接口(仅可在 C++ 中实现)可以使用特殊关键字 const 和 static 标记的方法在 C++ 中具有特殊效果:

special_methods = interface +c { 
     const accessor_method(); 
     static factory_method(); 
}
  • const 方法将在 C++ 中声明为 const,尽管这不能在其他语言中的调用者上强制执行,因为其他语言缺乏此功能。
  • 静态方法将成为C++类的静态方法,可以在没有对象的情况下从其他语言调用。 这对于用作跨语言构造函数的工厂方法通常很有用。

异常处理

当在 C++ 中实现的接口抛出 std::exception 时,它将被转换为 Java 中的 java.lang.RuntimeException 或 Objective-C 中的 NSException。 what() 消息也将被翻译。

常数

常量可以在接口和记录中定义。 在 Java 和 C++ 中,它们是生成类的一部分; 在 Objective-C 中,常量名称是全局变量,带有接口/记录名称的前缀。 例子:

record_with_const = record +c +j +o { 
     const const_value: i32 = 8; 
}

在 C++ 中将是 RecordWithConst::CONST_VALUE,在 Java 中将是 RecordWithConst.CONST_VALUE,在 Objective-C 中将是 RecordWithConstConstValue

模块化和库支持


当为您的项目生成接口并希望将其提供给所有 C++/Objective-C/Java 中的其他用户时,您可以告诉 Djinni 生成一个特殊的 YAML 文件作为代码生成过程的一部分。 该文件随后包含 Djinni 在不同项目中包含您的类型所需的所有信息。 指示 Djinni 创建这些 YAML 文件由以下参数控制:

  • --yaml-out: YAML 文件的输出文件夹(如果未指定,则禁用生成器)。
  • --yaml-out-file如果指定所有类型将合并到一个 YAML 文件中,而不是为每种类型生成一个文件(相对于 --yaml-out)。
  • --yaml-prefix :添加到存储在 YAML 文件中的类型名称的前缀(default: "")

这样的 YAML 文件如下所示:

---
name: mylib_record1
typedef: 'record +c deriving(eq, ord)'
params: []
prefix: 'mylib'
cpp:
    typename: '::mylib::Record1'
    header: '"MyLib/Record1.hpp"'
    byValue: false
objc:
    typename: 'MLBRecord1'
    header: '"MLB/MLBRecord1.h"'
    boxed: 'MLBRecord1'
    pointer: true
    hash: '%s.hash'
objcpp:
    translator: '::mylib::djinni::objc::Record1'
    header: '"mylib/djinni/objc/Record1.hpp"'
java:
    typename: 'com.example.mylib.Record1'
    boxed: 'com.example.mylib.Record1'
    reference: true
    generic: true
    hash: '%s.hashCode()'
jni:
    translator: '::mylib::djinni::jni::Record1'
    header: '"Duration-jni.hpp"'
    typename: jobject
    typeSignature: 'Lcom/example/mylib/Record1;'
---
name: mylib_interface1
typedef: 'interface +j +o'
    (...)
---
name: mylib_enum1
typedef: 'enum'
    (...)

YAML 文件中的每个文档都描述了一种 extern 类型。 example/example.yaml 中提供了所有字段的完整文档。 您还可以检查文件 test-suite/djinni/date.yaml test-suite/djinni/duration.yaml 以了解您可以使用它做什么的一些实际工作示例。

要在项目中使用库类型,只需将其包含在 IDL 文件中并使用其名称标识符引用它:

@extern "mylib.yaml"

client_interface = interface +c {
  foo(): mylib_record1;
}

只要您遵循所需的格式,就可以手动创建这些文件。 这允许您支持不是由 Djinni 生成的类型。 有关高级示例,请参阅 test-suite/djinni/duration.yaml 以及 test-suite/handwritten-src/cpp/Duration-objc.hpptest-suite/handwritten-src/cpp/Duration-jni.hpp 中的随附翻译器. 手写翻译器实现以下概念:

// For C++ <-> Objective-C
struct Record1
{
    using CppType = ::mylib::Record1;
    using ObjcType = MLBRecord1*;

    static CppType toCpp(ObjcType o) { return /* your magic here */; }
    static ObjcType fromCpp(CppType c) { return /* your magic here */; }

    // Option 1: use this if no boxing is required
    using Boxed = Record1;
    // Option 2: or this if you do need dedicated boxing behavior
    struct Boxed
    {
        using ObjcType = MLBRecord1Special*;
        static CppType toCpp(ObjcType o) { return /* your magic here */; }
        static ObjcType fromCpp(CppType c) { return /* your magic here */; }
    }
};
// For C++ <-> JNI
#include "djinni_support.hpp"
struct Record1
{
    using CppType = ::mylib::Record1;
    using JniType = jobject;

    static CppType toCpp(JniType j) { return /* your magic here */; }
    // The return type *must* be LocalRef<T> if T is not a primitive!
    static ::djinni::LocalRef<jobject> JniType fromCpp(CppType c) { return /* your magic here */; }

    using Boxed = Record1;
};

对于接口类,CppType 别名应为 std::shared_ptr<T>

确保将翻译器放入具有代表性和不同的命名空间。

如果您的类型是通用的,则翻译器采用相同数量的模板参数。 在使用时,每个都使用相应类型参数的翻译器进行实例化。

template<class A, class B>
struct Record1
{
    using CppType = ::mylib::Record1<typename A::CppType, typename B::CppType>;
    using ObjcType = MLBRecord1*;

    static CppType toCpp(ObjcType o)
    {
        // Use A::toCpp() and B::toCpp() if necessary
        return /* your magic here */;
    }
    static ObjcType fromCpp(CppType c)
    {
        // Use A::fromCpp() and B::fromCpp() if necessary
        return /* your magic here */;
    }

    using Boxed = Record1;
};

其他


构造函数/初始值设定项

Djinni 不允许为record或interface自定义构造函数,因为除非手动编辑自动生成的文件,否则无法在 Java 中实现它们。 相反,使用扩展记录或静态函数。

识别格式

Djinni 支持大多数生成的文件名和标识符的可覆盖格式。 可以通过使用 --help 调用 Djinni 来找到完整列表。 通过以所需的样式格式化单词 FooBar 来指定格式:

  • FOO_BAR -> GENERATED_IDENT
  • mFooBar -> mGeneratedIdent
  • FooBar -> GeneratedIdent

整数类型

在 Djinni 中,i8i64 都是固定长度使用的。 不使用 C++ 内置 intlong 等和 Objective-C NSInteger,因为它们的长度因架构而异。 不包括无符号整数,因为它们在 Java 中不可用。

测试套件


运行 make test 以调用 test-suite 子目录中的测试套件。 它将在本地 JVMy 上构建和运行 Java 代码,并在 iOS 模拟器上构建和运行 Objective-C。 后者只能在带有 Xcode 的 Mac 上运行。

生成一个独立的 jar


根目录Makefile 的 djinni_jar 目标创建了一个独立的 .jar。 这在引擎盖下使用了 sbt 程序集插件

只需从根目录调用:

make djinni_jar

这将在 src/target/scala_<SCALA_VERSION>/djinni-assembly-<VERSION>.jar 中生成一个 .jar 文件。

注意:生成jar需要sbt的环境,make djinni_jar前请确定正确安装sbt环境,如mac os运行以下命令安装

brew install sbt

您可以将其作为任何其他可执行文件 .jar 移动和使用。

假设 .jar 位于 $DJINNI_JAR_DIR 其版本等于 0.1-SNAPSHOT

# Example
java -jar $DJINNI_JAR_DIR/djinni-assembly-0.1-SNAPSHOT.jar \
    --java-out "$temp_out/java" \
    --java-package $java_package \
    --java-class-access-modifier "package" \
    --java-nullable-annotation "javax.annotation.CheckForNull" \
    --java-nonnull-annotation "javax.annotation.Nonnull" \
    --ident-java-field mFooBar \
    \
    --cpp-out "$temp_out/cpp" \
    --cpp-namespace textsort \
    --ident-cpp-enum-type foo_bar \
    \
    --jni-out "$temp_out/jni" \
    --ident-jni-class NativeFooBar \
    --ident-jni-file NativeFooBar \
    \
    --objc-out "$temp_out/objc" \
    --objcpp-out "$temp_out/objc" \
    --objc-type-prefix TXS \
    --objc-swift-bridging-header "TextSort-Bridging-Header" \
    \
    --idl "$in"

注意:根 Makefile 的 all 目标包括 djinni_jar 目标。

生成支持库的 iOS 通用二进制文件


ios-build-support-lib.sh 可帮助您为 iOS 平台构建通用静态库。 它使用 ios-cmake 存储库的平台文件。

它基本上为每个 IOS_PLATFORM 变量创建一个通用静态库,并使用 lipo 将所有文件合并为一个。

基本上有两个变量要修改:

Android Parcelable 序列化


Djinni 支持生成实现 android.os.parcelable 的类。 为此,需要两个步骤:

  • 使用关键字parcelable导出应该可分块的record: deriving(parcelable)
  • 运行时加入此命令 --java-implement-android-os-parcelable true

社区链接


  • Join the discussion with other developers at the Mobile C++ Slack Community
  • There are a set of tutorials for building a cross-platform app using Djinni.
  • mx3 is an example project demonstrating use of Djinni and other tools.
  • Slides and video from the CppCon 2014 talk where we introduced Djinni.
  • Slides and video from the CppCon 2015 talk about Djinni implementation techniques, and the addition of Python.
  • You can see a CppCon 2014 talk by app developers at Dropbox about their cross-platform experiences.

作者


  • Kannan Goundan
  • Tony Grue
  • Derek He
  • Steven Kabbes
  • Jacob Potter
  • Iulia Tamas
  • Andrew Twyman

联系我们


  • Andrew Twyman - artwymana+djinni@gmail.com
  • Jacob Potter - djinni@j4cbo.com
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 在 Djinni 中,可以使用 `std::string` 的构造函数来把 `std::vector<uint8_t>` 转换为 `std::string`。例如: ```cpp std::vector<uint8_t> buffer = …; std::string str(buffer.begin(), buffer.end()); ``` 注意,这种方法只适用于转换包含 ASCII 字符的 buffer,如果 buffer 中包含非 ASCII 字符,则会出现乱码。如果需要转换含有非 ASCII 字符的 buffer,则需要使用适当的字符编码,例如 UTF-8。可以使用以下代码来转换包含 UTF-8 编码的 buffer: ```cpp std::vector<uint8_t> buffer = …; std::string str(buffer.begin(), buffer.end()); std::wstring_convert<std::codecvt_utf8<wchar_t>> converter; std::wstring wide_str = converter.from_bytes(str); std::string utf8_str(wide_str.begin(), wide_str.end()); ``` 这里假设 buffer 中的字符均为 UTF-8 编码。如果 buffer 使用了其他字符编码,则需要使用对应的转换器来完成转换。 ### 回答2: 在Djinni中,将缓冲区转换为字符串的过程分为两个步骤。首先,通过将缓冲区的内容复制到一个临时`std::string`对象中来读取缓冲区的内容。然后,使用Djinni提供的转换函数将临时字符串转换为可在不同编程语言间传递的字符串类型。 为了将缓冲区转换为字符串,我们可以使用以下示例代码: ```cpp // 将缓冲区转换为字符串 std::string bufferToString(const djinni::ByteBuffer& buffer) { // 通过将缓冲区的内容复制到一个临时std::string对象中,读取缓冲区的内容 std::string tempString(buffer.begin(), buffer.end()); // 使用Djinni提供的转换函数将临时字符串转换为可传递给不同编程语言的字符串类型 djinni::String convertedString = djinni::String::fromCpp(tempString); // 返回转换后的字符串 return convertedString; } ``` 在这个函数中,我们首先将缓冲区的内容复制到一个临时`std::string`对象中,通过使用`buffer.begin()`和`buffer.end()`迭代器将缓冲区的开始和结束位置传递给构造函数。然后,我们使用Djinni提供的`fromCpp()`函数将临时字符串转换为可传递给其他编程语言的字符串类型。最后,我们将转换后的字符串返回。 以上是在Djinni中将缓冲区转换为字符串的简单示例。请注意,实际实现可能会依赖于具体的编程语言使用的编译器/工具链。 ### 回答3: 在Djinni中,将buffer转换为string的过程如下所示: 1. 首先,需要根据不同的编程语言选择适当的方法。Djinni支持不同的编程语言,如C++、Java和Objective-C等。 2. 如果在C++中使用Djinni,可以使用std::string类来表示字符串。可以通过将buffer的数据逐字节复制到std::string对象中来完成转换。 3. 如果在Java使用Djinni,可以使用java.nio.charset包中的Charset和CharsetDecoder类来实现转换。首先,需要确定使用哪种字符编码。然后,可以通过创建ByteBuffer对象,将buffer中的数据写入其中,并通过CharsetDecoder对象将其解码为字符串。 4. 如果在Objective-C中使用Djinni,可以使用NSString类来表示字符串。可以通过将buffer的数据转换为NSData对象,然后使用NSString的initWithData:encoding:方法将其解码为字符串。 需要注意的是,要正确转换buffer为string,必须确保buffer中的数据编码方式与字符串需要的编码方式一致。否则,可能会导致乱码或转换错误。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值