Skip to content

使用Luajit中的ffi

Smile Yik edited this page Oct 23, 2022 · 2 revisions

最后更新于2022年10月23日 | 历史记录

此页面内容对应于LuaInMinecraftBukkit插件的最新版本(及以上版本)(version: 1.7.1), 历史文档可以插件此页面的历史记录

使用Luajit中的ffi

在这个例子里面将会使用c++编写一个原始版本的apriori算法, 并使用lua脚本插件 让其在Minecraft服务器中运行.

ffi的使用方法可以在官网找到: ext_ffi

前置准备

在 LuaInMinecraftBukkit 插件目录下的 config.lua 配置文件中, 使用如下配置来启动 luagit2.1.0beta3

setting:native_version("luajit_2_1_0_beta3")

linux环境下使用

1. 创建一个文件名叫apriori_lib.cpp, 填入以下内容, 而不必刻意去在意apriori算法的实现
#include <iostream>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <stdio.h>
#include <algorithm>
using namespace std;

class itemset {
private:
  vector<string> items;

public:
  int support = 0;
  itemset() {
  }

  itemset(const vector<string>& tmp) {
    for (auto begin = tmp.begin(), end = tmp.end(); begin != end; ++begin) {
      items.push_back(*begin);
    }
  }

  itemset(string* strs, int strs_length) {
    for (auto begin = strs, end = strs + strs_length; begin != end; ++begin) {
      items.push_back(*begin);
    }
  }

  void add_item(string item) {
    // add item
    for (auto begin = items.begin(), end = items.end(); begin != end; ++begin) {
      if (*begin == item) {
        return;
      }
    }
    items.push_back(item);
  }

  void add_items(const itemset& set) {
    // add item
    for (auto begin = set.items.begin(), end = set.items.end(); begin != end; ++begin) {
      add_item(*begin);
    }
  }

  void all_subsets(int& length, vector<itemset>& subsets) {
    length = items.size();
    int i;
    for (i = 0; i < length; ++i) {
      string first = *items.begin();
      items.erase(items.begin());
      subsets.push_back(items);
      items.push_back(first);
    }
  }

  string to_string() {
    string s = "";
    for (auto begin = items.begin(), end = items.end(); begin != end; ++begin) {
      s += *begin + ", ";
    }
    return s + ::to_string(support);
  }

  void normalize() {
    sort(items.begin(), items.end());
  }

  bool operator==(const itemset& other) {
    normalize();
    return other.items == items;
  }

  bool is_in_itemsets(vector<itemset> frequent_itemset, const int& size) {
    int i;
    for (i = 0; i < size; ++i) {
      frequent_itemset.at(i).normalize();
      if (*this == frequent_itemset.at(i)) {
        return true;
      }
    }
    return false;
  }

  bool is_subset_of(const itemset& set) {
    std::set<string> tmp;
    for (auto begin = set.items.begin(), end = set.items.end(); begin != end; ++begin) {
      tmp.insert(*begin);
    }
    for (auto begin = items.begin(), end = items.end(); begin != end; ++begin) {
      if (!tmp.count(*begin)) {
        return false;
      }
    }
    return true;
  }

  const size_t size() {
    return items.size();
  }

  const vector<string> get_items() {
    return items;
  }
};

bool has_infrequent_subset(vector<itemset>& frequent_itemset, const int& fre_itemsets_size, itemset& candidate_itemset) {
  int size;
  vector<itemset> subsets;
  candidate_itemset.all_subsets(size, subsets);
  int i;
  for (i = 0; i < size; ++i) {
    if (!(subsets.at(i)).is_in_itemsets(frequent_itemset, fre_itemsets_size)) {
      return true;
    }
  }
  return false;
}

void apriori_gen(vector<itemset> d, const int min_sup, vector<itemset>& frequent_itemset, const int& fre_itemsets_size, vector<itemset>& new_itemsets) {
  int i, j;
  int size = frequent_itemset.at(0).size();
  set<itemset> checked;
  for (i = 0; i < fre_itemsets_size; ++i) {
    for (j = i + 1; j < fre_itemsets_size; ++j) {
      itemset tmp = itemset((frequent_itemset.at(i)).get_items());
      tmp.add_items(frequent_itemset.at(j));
      if (tmp.size() == size + 1 && !has_infrequent_subset(frequent_itemset, fre_itemsets_size, tmp)) {
        for (auto & b : d) {
          if (tmp.is_subset_of(b)) {
            tmp.support++;
          }
        }
        if (tmp.support >= min_sup) {
          tmp.normalize();
          bool flag = true;
          for (auto & it : new_itemsets) {
            if (it == tmp) {
              flag = false;
              break;
            }
          }
          if (flag) {
            new_itemsets.push_back(tmp);
          }
        }
      }
    }
  }
}

void apriori_gen_f1(vector<itemset>& d, vector<itemset>& f1, const int& min_sup) {
  map<string, int> tmp;
  auto begin = d.begin();
  auto end = d.end();
  while (begin != end) {
    const vector<string> items = begin->get_items();
    for (string s : items) {
      if (tmp.count(s)) {
        tmp[s]++;
      } else {
        tmp[s] = 1;
      }
    }
    ++begin;
  }

  for (auto & b : tmp) {
    if (min_sup > b.second) {
      continue;
    }
    itemset it;
    it.add_item(b.first);
    it.support = b.second;
    f1.push_back(it);
  }
}

void apripri(vector<itemset> d, int min_sup) {
  if (min_sup <= 0) {
    min_sup = 1;
  }
  vector<itemset> f1;
  apriori_gen_f1(d, f1, min_sup);
  while (!f1.empty()) {
    cout << "---------------" << endl;
    for (auto & begin : f1) {
      cout << begin.to_string() << endl;
    }
    vector<itemset> new_itemsets;
    apriori_gen(d, min_sup, f1, f1.size(), new_itemsets);
    f1 = new_itemsets;
  }

}

/*
 * 创建一个vector<itemset>并返回其指针.
 */
uint64_t new_itemsets() {
  vector<itemset>* itemsets = new vector<itemset>();
  return (uint64_t) itemsets;
}

/**
 * 根据vector<itemset>指针地址, 释放此指针
 */
void delete_itemsets(uint64_t itemsetsPtr) {
  vector<itemset>* itemsets = (vector<itemset>*) (itemsetsPtr);
  delete itemsets;
}

/**
 * 在vector<itemset>末尾创建一个itemset, 并返回其索引.
 */
int new_itemset(uint64_t itemsetsPtr) {
  vector<itemset>* itemsets = (vector<itemset>*) (itemsetsPtr);
  int idx = itemsets->size();
  itemsets->push_back(itemset());
  return idx;
}

/**
 * 根据vector<itemset>指针及itemset索引, 为itemset添加一个字符串.
 */
void add_item_to_set(uint64_t itemsetsPtr, int idx, char* str) {
  vector<itemset>* itemsets = (vector<itemset>*)(itemsetsPtr);
  (*itemsets)[idx].add_item(string(str));
  itemsets->push_back(itemset());
}

/**
 * 根据vector<itemset>指针及提供的最小支持度, 进行计算.
 */
void apripri(uint64_t itemsetsPtr, int min_sup) {
  vector<itemset>* itemsets = (vector<itemset>*)(itemsetsPtr);
  apripri(*itemsets, min_sup);
}

int main(int argc, char **argv)
{
    std::cout << "Hello, world!" << std::endl;
    string str1[] = {"I1", "I2", "I5"};
    string str2[] = {"I2", "I4"};
    string str3[] = {"I2", "I3"};
    string str4[] = {"I1", "I2", "I4"};
    string str5[] = {"I1", "I3"};
    string str6[] = {"I2", "I3"};
    string str7[] = {"I1", "I3"};
    string str8[] = {"I1", "I2", "I3", "I5"};
    string str9[] = {"I1", "I2", "I3"};

    vector<itemset> set1;
    set1.push_back(itemset(str1, 3));
    set1.push_back(itemset(str2, 2));
    set1.push_back(itemset(str3, 2));
    set1.push_back(itemset(str4, 3));
    set1.push_back(itemset(str5, 2));
    set1.push_back(itemset(str6, 2));
    set1.push_back(itemset(str7, 2));
    set1.push_back(itemset(str8, 4));
    set1.push_back(itemset(str9, 3));

    apripri(set1, 1);

    return 0;
}

在这个较长的代码里, 需要在意的代码仅仅只有如下这些, 因为我们在意的是如何利用 lua去调用这个算法, 而不是apripri算法的实现.

/*
 * 创建一个vector<itemset>并返回其指针.
 */
uint64_t new_itemsets();

/**
 * 根据vector<itemset>指针地址, 释放此指针
 */
void delete_itemsets(uint64_t itemsetsPtr);

/**
 * 在vector<itemset>末尾创建一个itemset, 并返回其索引.
 */
int new_itemset(uint64_t itemsetsPtr);

/**
 * 根据vector<itemset>指针及itemset索引, 为itemset添加一个字符串.
 */
void add_item_to_set(uint64_t itemsetsPtr, int idx, char* str);

/**
 * 根据vector<itemset>指针及提供的最小支持度, 进行计算.
 */
void apripri(uint64_t itemsetsPtr, int min_sup);
2. 将apriori_lib.cpp使用如下指令编译为动态链接库
c++ -shared -fPIC apriori_lib.cpp -o apriori_lib.so
3. 创建一个以Outside(Native)方式运行的插件, 在main.lua中写下如下代码:
local ffi = require "ffi"
local apriori = ffi.load(self:getRequirePath("c_libraries/apriori_lib.so"))

-- 加载函数, c++中的函数重载后会变函数名.
ffi.cdef[[
uint64_t _Z12new_itemsetsv();
void _Z15delete_itemsetsm(uint64_t itemsetsPtr);
int _Z11new_itemsetm(uint64_t itemsetsPtr);
void _Z15add_item_to_setmiPc(uint64_t itemsetsPtr, int idx, char* str);
void _Z7apriprimi(uint64_t itemsetsPtr, int min_sup);
]]

-- 多个购物篮中的物品名称数据
local strs = {
    {"I1", "I2", "I5"},
    {"I2", "I4"},
    {"I2", "I3"},
    {"I1", "I2", "I4"},
    {"I1", "I3"},
    {"I2", "I3"},
    {"I1", "I3"},
    {"I1", "I2", "I3", "I5"},
    {"I1", "I2", "I3"}
}


-- 当脚本插件被启用时会执行这个方法
function onEnable()
    -- 创建一个vector<itemset>指针
    local itemsets_ptr = apriori._Z12new_itemsetsv()
    for i = 1, #(strs) do
        local items = strs[i]
        -- 在vector<itemset>中新建一个itemset, 并返回其下标
        local idx = apriori._Z11new_itemsetm(itemsets_ptr)
        for j = 1, #(items) do
            -- 将购物篮里的物品名加入itemset中
            apriori._Z15add_item_to_setmiPc(
                itemsets_ptr, idx, ffi.cast("char*", items[j])
            )
        end
    end
    -- 使用apriori算法分析购物篮
    apriori._Z7apriprimi(itemsets_ptr, 2)
    -- 删除vector<itemset>指针
    apriori._Z15delete_itemsetsm(itemsets_ptr)
    print("加载完毕")
end

-- 当脚本插件被结束时会执行这个方法
function onDisable()
    print("卸载完毕")
end
4. 启动服务器, 将会看到如下信息
[18:31:06] [Server thread/INFO]: [LuaInMinecraftBukkit] Enabling LuaInMinecraftBukkit v1.7.1
[18:31:06] [Server thread/INFO]: [LuaInMinecraftBukkit] 版本已加载: LUAJIT_2_1_0_BETA3
[18:31:06] [Server thread/INFO]: [LuaInMinecraftBukkit] 正在启用混合模式.....
[18:31:06] [Server thread/INFO]: [LuaInMinecraftBukkit] 正在加载插件: apriori_test(apriori_test), 作者: SmileYik, 版本: 1.0
[18:31:06] [Server thread/INFO]: [LuaInMinecraftBukkit] 正在以Native模式启用插件: apriori_test(apriori_test), 作者: SmileYik, 版本: 1.0
>---------------
I1, 6
I2, 7
I3, 6
I4, 2
I5, 2
---------------
I1, I2, 4
I1, I3, 4
I1, I5, 2
I2, I3, 4
I2, I4, 2
I2, I5, 2
---------------
I1, I2, I3, 2
I1, I2, I5, 2
加载完毕[18:31:06] [Server thread/INFO]: [LuaInMinecraftBukkit] called closure [apriori_test, onEnable]: 2ms
[18:31:06] [Server thread/INFO]: Server permissions file permissions.yml is empty, ignoring it
[18:31:06] [Server thread/INFO]: Done (9.138s)! For help, type "help"
说明
  1. 因为c++支持方法重载, 故在其编译过程中会对函数名按照一定规则进行重命名, 这就是为什么 在ffi.cdef中, 声明的函数名与之前编写的函数名不一样, 要较为方便的获取重命名后的函数名, 在linux系统下可以使用命令去查找
nm -D <动态链接库> | grep <函数名>

举个例子, 就以上写的代码, 我想查找add_item_to_set函数编译后的函数名, 可以这样:

❯ nm -D apriori_lib.so | grep add_item_to_set
000000000001d15d T _Z15add_item_to_setmiPc

这样查找出来的函数名就是_Z15add_item_to_setmiPc, 之后在ffi.cdef中这样声明

ffi.cdef[[
void _Z15add_item_to_setmiPc(uint64_t itemsetsPtr, int idx, char* str);
]]

这样就能正常使用了, 要是嫌弃函数名称难读, 可以这样重命名函数名为你想要的函数名

local ffi = require "ffi"
local apriori_lib = ffi.load(self:getRequirePath("c_libraries/apriori_lib.so"))
ffi.cdef[[
uint64_t _Z12new_itemsetsv();
void _Z15delete_itemsetsm(uint64_t itemsetsPtr);
int _Z11new_itemsetm(uint64_t itemsetsPtr);
void _Z15add_item_to_setmiPc(uint64_t itemsetsPtr, int idx, char* str);
void _Z7apriprimi(uint64_t itemsetsPtr, int min_sup);
]]

-- 重新命名函数名
local apriori = {
    add_item_to_set = apriori_lib._Z15add_item_to_setmiPc,
    new_itemsets = apriori_lib._Z12new_itemsetsv,
    delete_itemsets = apriori_lib._Z15delete_itemsetsm,
    new_itemset = apriori_lib._Z11new_itemsetm,
    apripri = apriori_lib._Z7apriprimi
}

之后使用 apriori 变量去调用对应函数即可