SEAL中用CRT完成多项式编码相关(SEAL/Polycrt.h 2.3.0)


终于还是要读源码了,终究摆脱不了数学的噩梦,先从读懂他们的代码开始。
文件原位置SEAL/Polycrt.h

介绍

提供CRT批处理功能。 如果多项式模数是 x N + 1 x^N+1 xN+1,并且明文模数是素数 T T T 使得 T ≡ 1 m o d    2 N T \equiv 1 \mod 2N T1mod2N ,则PolyCRTBuilder允许SEAL明文元素被视为 2 × ( N / 2 ) 2 \times (N/2) 2×(N/2) 的矩阵,在这种加密矩阵上执行的同态操作是应用系数(槽),为可矢量化的计算启用强大的SIMD功能。 此功能通常在同态加密文献中称为“批处理”。

  • par数学背景
    从数学上讲,如果poly_modulus是 x N + 1 x^N+1 xN+1 N N N ​ ​ 2 ​​2 2 的幂,plain_modulus是素数 T T T,使得 2 N 2N 2N 整除 T − 1 T-1 T1,则模 T T T的整数包含 一个 2 N 2N 2N 的原始根,多项式 X N + 1 X ^ N + 1 XN+1分裂成 n n n个不同的线性因子,如 X N + 1 = ( X − a 1 ) ∗ ⋯ ∗ ( X − a N ) m o d    T X ^ N + 1 =(X-a_1)*\dots *(X-a_N)\mod T XN+1=Xa1XaNmodT,其中常数 a 1 , … , a n a_1,\dots ,a_n a1an 都是不同的原始的第 2 N 2N 2N个整数的根在整数模 T T T中。中国剩余定理(CRT)表明在这种情况下明文空间 Z T [ X ] / ( X N + 1 ) Z_T [X] /(X ^ N + 1) ZT[X]/(XN+1) N N N同构(作为代数)折叠领域的直接产品 Z T Z_T ZT。同构很容易在两个方向上明确计算,这就是这个类的作用。此外,扩张的伽罗瓦群是 ( Z / 2 N Z ) ∗ 〜 = Z / 2 Z × Z / ( N / 2 ) (Z / 2NZ)*〜= Z / 2Z×Z /(N / 2) (Z/2NZ)=Z/2Z×Z/(N/2),其对原始根的作用很容易描述。由于批处理时隙与统一的原始根对应1对1,因此通过置换时隙在明文上应用Galois自同构。通过应用伽罗瓦群的两个循环子群的生成器,我们可以有效地将明文视为2乘 N / 2 N / 2 N/2 矩阵,并且能够实现循环行旋转和列旋转(行交换)。

  • par有效参数
    是否可以使用批处理取决于是否适当选择了明文模数。 因此,要构造PolyCRTBuilder,用户必须提供SEALContext的实例,以使其关联的EncryptionParameterQualifiers对象将flags_set和enable_batching设置为true。

  • par重载
    对于分解函数,我们提供了两个有关操作期间所需分配中使用的内存池的重载。 在一次重载中,PolyCRTBuilder的本地内存池(用于存储预计算结果和其他成员变量)用于此目的,而在另一个重载中,用户可以提供要使用的MemoryPoolHandle。 这是为了允许多个线程同时使用一个PolyCRTBuilder,而不会在操作期间发生的分配中遇到线程争用。 例如,可以跨任意数量的线程共享一个PolyCRTBuilder,但是在每个线程中通过为其提供一个线程本地MemoryPoolHandle来调用加密函数。 对于开发人员而言,了解其工作原理以避免不必要的性能瓶颈非常重要。

    @有关加密参数的详细信息,请参阅EncryptionParameters。
    @see EncryptionParameterQualifiers了解有关参数限定符的更多信息。
    @see Evaluator用于旋转加密矩阵的行和列。

overview

polycrtbuilder

构造函数

基本构造

PolyCRTBuilder(const SEALContext &context, const MemoryPoolHandle &pool = MemoryPoolHandle::Global());

创建PolyCRTBuilder。 通过SEALContext对象提供的加密参数必须支持批处理。 动态分配的成员变量是从给定的MemoryPoolHandle指向的内存池中分配的。 默认情况下,使用全局内存池。

     @param [in] context SEALContext
     @param [in] pool MemoryPoolHandle 指向有效的内存池
     如果加密参数对批处理无效,则@throws std :: invalid_argument
     如果池未初始化,则@throws std :: invalid_argument
    PolyCRTBuilder::PolyCRTBuilder(const SEALContext &context, const MemoryPoolHandle &pool) :
        pool_(pool), parms_(context.parms()),
        ntt_tables_(pool_),
        slots_(parms_.poly_modulus().coeff_count() - 1),
        qualifiers_(context.qualifiers())
    {
        int coeff_count = parms_.poly_modulus().coeff_count();

        // Verify parameters
        if (!qualifiers_.parameters_set)
        {
            throw invalid_argument("encryption parameters are not set correctly");
        }
        if (!qualifiers_.enable_batching)
        {
            throw invalid_argument("encryption parameters are not valid for batching");
        }
        if (!pool)
        {
            throw invalid_argument("pool is uninitialized");
        }

        // Set mod_ and polymod_
        mod_ = parms_.plain_modulus();
        polymod_ = PolyModulus(parms_.poly_modulus().pointer(), coeff_count, 
            parms_.poly_modulus().coeff_uint64_count());

        // Reserve space for all of the primitive roots
        roots_of_unity_ = allocate_uint(slots_, pool_);

        // Copy over NTT tables (switching to local pool)
        ntt_tables_ = context.plain_ntt_tables_;

        // Fill the vector of roots of unity with all distinct odd powers of generator.
        // These are all the primitive (2*slots_)-th roots of unity in integers modulo parms_.plain_modulus().
        populate_roots_of_unity_vector();

        // Populate matrix representation index map
        populate_matrix_reps_index_map();
    }

深拷贝

PolyCRTBuilder(const PolyCRTBuilder &copy);

创建给定PolyCRTBuilder的深拷贝。

     @param [in] copy 要复制的PolyCRTBuilder
PolyCRTBuilder::PolyCRTBuilder(const PolyCRTBuilder &copy) :
        pool_(copy.pool_), parms_(copy.parms_),
        ntt_tables_(copy.ntt_tables_),
        slots_(copy.slots_),
        qualifiers_(copy.qualifiers_)
    {
        int coeff_uint64_count = parms_.plain_modulus().uint64_count();

        // Allocate and copy over roots of unity
        roots_of_unity_ = allocate_poly(slots_, coeff_uint64_count, pool_);
        set_poly_poly(copy.roots_of_unity_.get(), slots_, coeff_uint64_count, roots_of_unity_.get());

        // Set mod_ and polymod_
        mod_ = parms_.plain_modulus();
        polymod_ = PolyModulus(parms_.poly_modulus().pointer(), parms_.poly_modulus().coeff_count(), 
            parms_.poly_modulus().coeff_uint64_count());
    }

移动

PolyCRTBuilder(PolyCRTBuilder &&source) = default;

通过移动给定的一个来创建一个新的PolyCRTBuilder。

     @param [in] source 要移动的PolyCRTBuilder

编码(compose)

uint64_t

void compose(const std::vectorstd::uint64_t &values, Plaintext &destination);

从给定矩阵创建SEAL明文。 该函数将以明文模数为模的给定整数矩阵“批处理”为SEAL明文元素,并将结果存储在目标参数中。 输入向量的大小必须至多等于多项式模数的大小。 元素的前半部分代表矩阵的第一行,后半部分代表第二行。 矩阵中的大小最大等于明文模数,以表示有效的SEAL明文。

     @param [in] values 整数矩阵模数为明文模数
     @param [out] destination 用结果覆盖的明文多项式
     @throws std :: invalid_argument 如果值太大,抛出异常
void PolyCRTBuilder::compose(const vector<int64_t> &values_matrix, Plaintext &destination)
    {
        // Validate input parameters,验证输入的参数,如果矩阵的大小大于crtbuilder 的slot_counts,则报错。
        if (values_matrix.size() > slots_)
        {
            throw logic_error("values_matrix size is too large");
        }
#ifdef SEAL_DEBUG
        for (auto v : values_matrix)
        {
            // Validate the i-th input
            if (v >= mod_.value())
            {
                throw invalid_argument("input value is larger than plain_modulus");
            }
        }
#endif
        // 当前输入矩阵的大小
        int input_matrix_size = values_matrix.size();

        // Set destination to full size,将明文多项式的目标位置预先设置为slot_counts        
        destination.resize(slots_);

        // First write the values to destination coefficients. Read in top row, then bottom row. 不全的数组数据用0补充。
        for (int i = 0; i < input_matrix_size; i++)
        {
            *(destination.pointer() + matrix_reps_index_map_[i]) = values_matrix[i];
        }        
        for (int i = input_matrix_size; i < slots_; i++)
        {
            *(destination.pointer() + matrix_reps_index_map_[i]) = 0;
        }

        // Transform destination using inverse of negacyclic NTT
        // Note: We already performed bit-reversal when reading in the matrix
        inverse_ntt_negacyclic_harvey(destination.pointer(), ntt_tables_);

int64_t

void compose(const std::vectorstd::int64_t &values, Plaintext &destination);

同上,只类型不同。
从给定矩阵创建SEAL明文。 该函数将以明文模数为模的给定整数矩阵“批处理”为SEAL明文元素,并将结果存储在目标参数中。 输入向量的大小必须至多等于多项式模数的大小。 元素的前半部分代表矩阵的第一行,后半部分代表第二行。 矩阵中的数字最多等于明文模数,以表示有效的SEAL明文。

     @param [in] values 整数矩阵模数为明文模数
     @param [out] destination 用结果覆盖的明文多项式
     @throws std :: invalid_argument 如果值太大
void PolyCRTBuilder::compose(const vector<int64_t> &values_matrix, Plaintext &destination)
    {
        // Validate input parameters
        if (values_matrix.size() > slots_)
        {
            throw logic_error("values_matrix size is too large");
        }

        uint64_t plain_modulus_div_two = mod_.value() >> 1;
#ifdef SEAL_DEBUG
        for (auto v : values_matrix)
        {
            // Validate the i-th input
            if (abs(v) > plain_modulus_div_two)
            {
                throw invalid_argument("input value is larger than plain_modulus");
            }
        }
#endif
        int input_matrix_size = values_matrix.size();

        // Set destination to full size
        destination.resize(slots_);

        // First write the values to destination coefficients. Read 
        // in top row, then bottom row.
        for (int i = 0; i < input_matrix_size; i++)
        {
            *(destination.pointer() + matrix_reps_index_map_[i]) = (values_matrix[i] < 0) ? 
                (mod_.value() + values_matrix[i]) : values_matrix[i];
        }
        for (int i = input_matrix_size; i < slots_; i++)
        {
            *(destination.pointer() + matrix_reps_index_map_[i]) = 0;
        }

        // Transform destination using inverse of negacyclic NTT
        // Note: We already performed bit-reversal when reading in the matrix
        inverse_ntt_negacyclic_harvey(destination.pointer(), ntt_tables_);
    }

plaintext

void PolyCRTBuilder::compose(Plaintext &plain, const MemoryPoolHandle &pool)

从给定矩阵创建SEAL明文。该函数将给定的整数模量的整数给定矩阵“批处理”为准备加密的SEAL明文。矩阵作为明文元素给出,其前 N / 2 N / 2 N/2 系数表示矩阵的第一行,后 N / 2 N / 2 N/2系数表示第二行,其中 N N N 表示多项式模数的程度。输入明文必须具有小于多项式模数的递减,并且系数小于明文模数,即它必须是加密参数的有效明文。进程中的动态内存分配是从给定MemoryPoolHandle指向的内存池中分配的。

    @param [in] plain整数矩阵模数为明文模数
    @param [in] pool MemoryPoolHandle指向有效的内存池
    如果plain对加密参数无效,则@throws std :: invalid_argument
    如果池未初始化,则@throws std :: invalid_argument
    * /
 void PolyCRTBuilder::compose(Plaintext &plain, const MemoryPoolHandle &pool)
    {
        int coeff_count = parms_.poly_modulus().coeff_count();

        // Validate input parameters
        if (plain.coeff_count() > coeff_count || 
            (plain.coeff_count() == coeff_count && plain[coeff_count - 1] != 0))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#ifdef SEAL_DEBUG
        if (plain.significant_coeff_count() >= coeff_count || !are_poly_coefficients_less_than(plain.pointer(),
            plain.coeff_count(), 1, parms_.plain_modulus().pointer(), parms_.plain_modulus().uint64_count()))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#endif
        if (!pool)
        {
            throw invalid_argument("pool is uninitialized");
        }

        // We need to permute the coefficients of plain. To do this, we allocate 
        // temporary space.
        int input_plain_coeff_count = min(plain.coeff_count(), slots_);
        Pointer temp(allocate_uint(input_plain_coeff_count, pool));
        set_uint_uint(plain.pointer(), input_plain_coeff_count, temp.get());

        // Set plain to full slot count size.
        plain.resize(slots_);

        // First write the values to destination coefficients. Read 
        // in top row, then bottom row.
        for (int i = 0; i < input_plain_coeff_count; i++)
        {
            *(plain.pointer() + matrix_reps_index_map_[i]) = temp[i];
        }
        for (int i = input_plain_coeff_count; i < slots_; i++)
        {
            *(plain.pointer() + matrix_reps_index_map_[i]) = 0;
        }

        // Transform destination using inverse of negacyclic NTT
        // Note: We already performed bit-reversal when reading in the matrix
        inverse_ntt_negacyclic_harvey(plain.pointer(), ntt_tables_);
    }

解码(decompose)

uint64_t

void PolyCRTBuilder::decompose(const Plaintext &plain, vector<uint64_t> &destination, const MemoryPoolHandle &pool)

iverse of compose。 该函数将给定SEAL明文“解”为以明文模数为模的整数矩阵,并将结果存储在目标参数中。 输入明文必须具有小于多项式模数的递减,并且系数小于明文模数,即它必须是加密参数的有效明文。 进程中的动态内存分配是从给定MemoryPoolHandle指向的内存池中分配的。

     @param [in] plain要解开的明文多项式
     @param [out] destination要用槽的值覆盖的向量
     @param [in] pool MemoryPoolHandle指向有效的内存池
     如果plain对加密参数无效,则@throws std :: invalid_argument
     如果池未初始化,则@throws std :: invalid_argument
void PolyCRTBuilder::decompose(const Plaintext &plain, vector<uint64_t> &destination, const MemoryPoolHandle &pool) 
    {
        int coeff_count = parms_.poly_modulus().coeff_count();

        // Validate input parameters
        if (plain.coeff_count() > coeff_count || 
            (plain.coeff_count() == coeff_count && plain[coeff_count - 1] != 0))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#ifdef SEAL_DEBUG
        if (plain.significant_coeff_count() >= coeff_count || !are_poly_coefficients_less_than(plain.pointer(),
            plain.coeff_count(), 1, parms_.plain_modulus().pointer(), parms_.plain_modulus().uint64_count()))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#endif
        if (!pool)
        {
            throw invalid_argument("pool is uninitialized");
        }

        // Set destination size
        destination.resize(slots_);

        // Never include the leading zero coefficient (if present)
        int plain_coeff_count = min(plain.coeff_count(), slots_);

        Pointer temp_dest(allocate_uint(slots_, pool));

        // Make a copy of poly
        set_uint_uint(plain.pointer(), plain_coeff_count, temp_dest.get());
        set_zero_uint(slots_ - plain_coeff_count, temp_dest.get() + plain_coeff_count);

        // Transform destination using negacyclic NTT.
        ntt_negacyclic_harvey(temp_dest.get(), ntt_tables_);

        // Read top row
        for (int i = 0; i < slots_; i++)
        {
            destination[i] = temp_dest[matrix_reps_index_map_[i]];
        }
    }

int64_t

void PolyCRTBuilder::decompose(const Plaintext &plain, vector<int64_t> &destination, const MemoryPoolHandle &pool)

同上

void PolyCRTBuilder::decompose(const Plaintext &plain, vector<int64_t> &destination,
        const MemoryPoolHandle &pool)
    {
        int coeff_count = parms_.poly_modulus().coeff_count();

        // Validate input parameters
        if (plain.coeff_count() > coeff_count || 
            (plain.coeff_count() == coeff_count && plain[coeff_count - 1] != 0))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#ifdef SEAL_DEBUG
        if (plain.significant_coeff_count() >= coeff_count || !are_poly_coefficients_less_than(plain.pointer(),
            plain.coeff_count(), 1, parms_.plain_modulus().pointer(), parms_.plain_modulus().uint64_count()))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#endif
        if (!pool)
        {
            throw invalid_argument("pool is uninitialized");
        }

        // Set destination size
        destination.resize(slots_);

        // Never include the leading zero coefficient (if present)
        int plain_coeff_count = min(plain.coeff_count(), slots_);

        Pointer temp_dest(allocate_uint(slots_, pool));

        // Make a copy of poly
        set_uint_uint(plain.pointer(), plain_coeff_count, temp_dest.get());
        set_zero_uint(slots_ - plain_coeff_count, temp_dest.get() + plain_coeff_count);

        // Transform destination using negacyclic NTT.
        ntt_negacyclic_harvey(temp_dest.get(), ntt_tables_);

        // Read top row, then bottom row
        uint64_t plain_modulus_div_two = mod_.value() >> 1;
        for (int i = 0; i < slots_; i++)
        {
            int64_t curr_value = temp_dest[matrix_reps_index_map_[i]];
            destination[i] = (curr_value > plain_modulus_div_two) ?
                (curr_value - mod_.value()) : curr_value;
        }
    }

plaintext

void PolyCRTBuilder::decompose(Plaintext &plain, const MemoryPoolHandle &pool)

void PolyCRTBuilder::decompose(Plaintext &plain, const MemoryPoolHandle &pool)
    {
        int coeff_count = parms_.poly_modulus().coeff_count();

        // Validate input parameters
        if (plain.coeff_count() > coeff_count || 
            (plain.coeff_count() == coeff_count && plain[coeff_count - 1] != 0))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#ifdef SEAL_DEBUG
        if (plain.significant_coeff_count() >= coeff_count || !are_poly_coefficients_less_than(plain.pointer(),
            plain.coeff_count(), 1, parms_.plain_modulus().pointer(), parms_.plain_modulus().uint64_count()))
        {
            throw invalid_argument("plain is not valid for encryption parameters");
        }
#endif
        if (!pool)
        {
            throw invalid_argument("pool is uninitialized");
        }

        // Never include the leading zero coefficient (if present)
        int plain_coeff_count = min(plain.coeff_count(), slots_);

        // Allocate temporary space to store a wide copy of plain
        Pointer temp(allocate_uint(slots_, pool));

        // Make a copy of poly
        set_uint_uint(plain.pointer(), plain_coeff_count, temp.get());
        set_zero_uint(slots_ - plain_coeff_count, temp.get() + plain_coeff_count);

        // Transform destination using negacyclic NTT.
        ntt_negacyclic_harvey(temp.get(), ntt_tables_);

        // Set plain to full slot count size (note that all new coefficients are 
        // set to zero).
        plain.resize(slots_);

        // Read top row, then bottom row
        for (int i = 0; i < slots_; i++)
        {
            *(plain.pointer() + i) = temp[matrix_reps_index_map_[i]];
        }
    }

其他

void PolyCRTBuilder::populate_roots_of_unity_vector()

    void PolyCRTBuilder::populate_roots_of_unity_vector()
    {
        uint64_t generator_sq = multiply_uint_uint_mod(ntt_tables_.get_root(), ntt_tables_.get_root(), mod_);
        roots_of_unity_[0] = ntt_tables_.get_root();

        for (int i = 0; i < slots_ - 1; i++)
        {
            roots_of_unity_[i + 1] = multiply_uint_uint_mod(roots_of_unity_[i], generator_sq, mod_);
        }
    }

void PolyCRTBuilder::populate_matrix_reps_index_map()

    void PolyCRTBuilder::populate_matrix_reps_index_map()
    {
        int logn = get_power_of_two(slots_);
        uint32_t row_size = slots_ >> 1;
        matrix_reps_index_map_.resize(slots_);

        // Copy from the matrix to the value vectors 
        uint32_t gen = 3;
        uint32_t pos = 1;
        uint32_t m = slots_ << 1;
        for (uint32_t i = 0; i < row_size; i++)
        {
            // Position in normal bit order
            uint32_t index1 = (pos - 1) >> 1;
            uint32_t index2 = (m - pos - 1) >> 1;

            // Set the bit-reversed locations
            matrix_reps_index_map_[i] = util::reverse_bits(index1, logn);
            matrix_reps_index_map_[row_size | i] = util::reverse_bits(index2, logn);

            // Next primitive root
            pos *= gen;
            pos &= (m - 1);
        }
    }

inline int slot_count()

inline int slot_count() const
        {
            return slots_;
        }

inline void reverse_bits(std::uint64_t *input)

inline void reverse_bits(std::uint64_t *input)
        {
#ifdef SEAL_DEBUG
            if (input == nullptr)
            {
                throw std::invalid_argument("input cannot be null");
            }
#endif
            std::uint32_t n = parms_.poly_modulus().coeff_count() - 1;
            int logn = util::get_power_of_two(n);
            for (std::uint32_t i = 0; i < n; i++)
            {
                std::uint32_t reversed_i = util::reverse_bits(i, logn);
                if (i < reversed_i)
                {
                    std::swap(input[i], input[reversed_i]);
                }
            }
        }
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值