ページ

ラベル Ruby の投稿を表示しています。 すべての投稿を表示
ラベル Ruby の投稿を表示しています。 すべての投稿を表示

2016年8月13日土曜日

Rubyでdllやsoを扱うFiddleのまとめ


最近仕事でお世話になっているFiddleのまとめです

Fiddleとは?
Fiddleが何かというと、Rubyの標準ライブラリに含まれている、
Rubyからdllやsoファイルを読み込んで使えるものです。
dlと同等の機能を持つ。dl自体はRuby2.0 以降deprecated となり、
2.2.0 で削除されてます。

簡単なサンプル


まずは簡単なサンプルを作ってみたいと思います。
適当なディレクトリに以下の構成で作成します。
.
├── CMakeLists.txt
├── build ※ディレクトリ
├── sample.c
└── sample.rb
それぞれのファイルを編集・・
  • CMakeLists.txt
# CMakeのバージョン
cmake_minimum_required(VERSION 2.8)

# プロジェクト名
project(sample C)

# 共有ライブラリを作成
add_library(sample SHARED sample.c)

# MacOSの場合.dylibで作成される為、.soで作成されるように以下を追記
if (APPLE)
    set_property(TARGET sample PROPERTY PREFIX "lib")
    set_property(TARGET sample PROPERTY OUTPUT_NAME "sample.so")
    set_property(TARGET sample PROPERTY SUFFIX "")
endif()
  • sample.c
#include <stdio.h>

int add(int a, int b) {
  return a + b;
}

void print(const char *msg) {
  puts(msg);
}
  • sample.rb
# coding: utf-8
require "fiddle/import"

module M
  extend Fiddle::Importer
  dlload "build/libsample.so" # 共有ライブラリをロード
  extern "int add(int, int)" # 関数をモジュールに教えてあげる
  extern "void print(const char *)"
end

# いざ実行!!
puts M.add(1, 2) #=> 3

M.print('hoge') #=> hoge
共有ライブラリ(so)側は単純に引数を加算して返すだけのadd関数と
引数の文字列を表示するprint関数を実装。
これをsample.rbで呼び出して実行しています。
ビルドして実行します。
$ cd build && cmake .. && make && cd ..
$ ruby sample.rb
こんな感じで単純な関数を呼ぶくらいであれば、
お気軽に使うことができます。

複雑なサンプル


実際にゴリゴリ使うとなったら上のサンプルのようにはいきません
例えばこんな構造体があったとします・・・
struct Person {
  int no;
  wchar_t name[20];
};

struct Members {
  struct Person persons[3];
  struct Members *nextPtr;
};
これをrubyから扱うとかちょっと考えたくないですが、
実際にやるとこうなります。
  • sample.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>

struct Person {
  int no;
  wchar_t name[20];
};

struct Members {
  struct Person persons[3];
  struct Members *nextPtr;
};

void initMember(struct Members **ppMembers) {
  *ppMembers = (struct Members *)malloc(sizeof(struct Members));
  (*ppMembers)->persons[0].no = 1;
  memset((*ppMembers)->persons[0].name, 0, sizeof(wchar_t) * 20);
  wcscpy((*ppMembers)->persons[0].name, L"hogehoge1");
  (*ppMembers)->persons[1].no = 2;
  memset((*ppMembers)->persons[1].name, 0, sizeof(wchar_t) * 20);
  wcscpy((*ppMembers)->persons[1].name, L"hogehoge2");
  (*ppMembers)->persons[2].no = 3;
  memset((*ppMembers)->persons[2].name, 0, sizeof(wchar_t) * 20);
  wcscpy((*ppMembers)->persons[2].name, L"hogehoge3");

  // Next pointer
  struct Members *next = (struct Members *)malloc(sizeof(struct Members));
  next->persons[0].no = 11;
  memset(next->persons[0].name, 0, sizeof(wchar_t) * 20);
  wcscpy(next->persons[0].name, L"fugafuga1");
  next->persons[1].no = 12;
  memset(next->persons[1].name, 0, sizeof(wchar_t) * 20);
  wcscpy(next->persons[1].name, L"fugafuga2");
  next->persons[2].no = 13;
  memset(next->persons[2].name, 0, sizeof(wchar_t) * 20);
  wcscpy(next->persons[2].name, L"fugafuga3");
  (*ppMembers)->nextPtr = next;
}

void freeMember(struct Members **ppMembers) {
  free((*ppMembers)->nextPtr);
  free(*ppMembers);
}
サンプルなのでエラー等は一切考慮してません。
  • sample.rb
# coding: utf-8
require "fiddle/import"

module M
  extend Fiddle::Importer
  dlload "build/libsample.so"
  extern "void initMember(void *)"
  extern "void freeMember(void *)"

  # 構造体をruby用に定義
  # 構造体in構造体は展開して定義しないといけない
  # wchar_tはMacOSだと4byte, windowsだと2byte
  # 今回MacOSなのでintをあてがってます
  MEMBERS = struct([
    "int no1",
    "int name1[20]",
    "int no2",
    "int name2[20]",
    "int no3",
    "int name3[20]",
    "void *nextPtr"
    ]);
end

# void *分の領域を確保
pointer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
M.initMember(pointer)
# Members構造体へキャスト
members = M::MEMBERS.new(pointer.ptr)
puts members.no1 #=> 1
puts members.name1.pack('U*') #=> hogehoge1
puts members.no2 #=> 2
puts members.name2.pack('U*') #=> hogehoge2
puts members.no3 #=> 3
puts members.name3.pack('U*') #=> hogehoge3

# nextPtrをMembers構造体へキャスト
members2 = M::MEMBERS.new(members.nextPtr)
puts members2.no1 #=> 11
puts members2.name1.pack('U*') #=> fugafuga1
puts members2.no2 #=> 12
puts members2.name2.pack('U*') #=> fugafuga2
puts members2.no3 #=> 13
puts members2.name3.pack('U*') #=> fugafuga3

M.freeMember(pointer)
とまあ、何とも強引な感じになりますw

2016年4月14日木曜日

MechanizeでWeb操作 3 - GithubのトレンドをKindleで読む -


前回の続きで、 今回は収集した情報をKindle端末にメール送信してみたいと思います。

メール送信


メール送信するに当たりGemfileに以下のgemを追加します。
gem "mail"
main.rbで追加したgemをrequireします
require 'mail'
メール送信部分の処理を追加します。gmailを使って送信を行います。
# 送信先メールアドレス(Kindle端末)
SEND_TO = 'XXXXXXX@kindle.com'.freeze
# 送信元メールアドレス(Gmail)
GMAIL_ADR = 'XXXXXXX@gmail.com'.freeze
# Gmailアカウントのパスワード
PASSWORD = 'XXXXXXXXXXXX'.freeze
# メール件名
SUBJECT = 'Github trends for a week.'.freeze

# メール送信メソッド
def send_mail

  # Gmail用のsmtpオプション
  options = {
            :address              => "smtp.gmail.com",
            :port                 => 587,
            :domain               => "smtp.gmail.com",
            :user_name            => GMAIL_ADR,
            :password             => PASSWORD,
            :authentication       => :plain,
            :enable_starttls_auto => true  }

  mail = Mail.new
  mail.to = SEND_TO
  mail.from = GMAIL_ADR
  mail.subject = SUBJECT
  mail.delivery_method(:smtp, options)
#  mail.add_file(※添付ファイル)

  mail.deliver
end
メールアドレスやパスワードは置き換えて下さい。
Net::SMTPAuthenticationErrorが発生した場合は認証の設定が必要です。
セキュリティ上、不明なアプリからのログインは通常認められていない為、
  • 安全性の低いアプリからのアクセスを許可する
  • 2段階認証をONにしてアプリ用のパスワードを発行する
のいずれかを行う必要があります。
アカウント設定から設定できます。

メールに添付するhtmlドキュメント作成


Kindleに送信するメールの添付ファイルとして今回はhtmlドキュメントを
添付します。※添付可能なフォーマットは前回を参照
処理としては、Mechanizeで取得したトレンドデータをストックし、
erbを使ってhtmlに展開します。
# Hashキー : リポジトリ名
KEY_REPOSITORY = :repository.freeze
# Hashキー : 作者名
KEY_AUTHOR = :author.freeze
# Hashキー : readme
KEY_README = :readme.freeze

# テンプレートファイル名
TEMPLATE_FILE = 'trends.html.erb'.freeze
# 作成htmlドキュメントファイル名
OUTPUT_FILE = 'trends.html'.freeze

# htmlドキュメント作成メソッド
def gen_html(trends)
  erb = ERB.new(File.read(TEMPLATE_FILE), nil, '-')
  ret = erb.result(binding)
  File.open(OUTPUT_FILE, "w:utf-8") do |f|
    f.write(ret)
  end
end

agent = Mechanize.new
agent.verify_mode = OpenSSL::SSL::VERIFY_NONE
page = agent.get('https://github.com/trending?since=weekly')

trends = []
page.search('h3 a').each do |link|
  trend = {}
  array = link.text.gsub(/(\s)/,"").split('/')
  # リポジトリ名の取得
  trend[KEY_REPOSITORY] = array[1]
  # 作者名の取得
  trend[KEY_AUTHOR] = array[0]

  # リポジトリのページへ遷移
  url = "#{GITHUB_TOP}#{link[:href]}"
  next_page = agent.get(url)
  # readmeの情報取得
  readme = next_page.at('#readme')
  trend[KEY_README] = readme
  # 配列に追加
  trends << trend
end

# html作成
gen_html(trends)
  • テンプレートファイル
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <title>Github trends.</title>
  </head>
  <body>
<% trends.each do |trend| -%>
  <h1><%= trend[:repository] %></h1>
  <p><%= trend[:author] %></p>
  <hr>
  <pre><%= trend[:readme] %></pre>
  <br>
<% end -%>
  </body>
</html>

完成


これで一通りできたので、実際に試してみます。
Kindle端末に送信されれば成功です。
  • 送信されたドキュメントを開いた様子



画像やgifなどが表示されないのが残念な感じですが ><;
一応トレンドのリポジトリとreadme一覧がKindleで読めます^^;
今回はテストでやってみましたが、今後色々と改良していきたいと思います。
  • 完成したソース
# coding: utf-8
require 'erb'
require 'mail'
require 'mechanize'

GITHUB_TOP = 'https://github.com'.freeze

# Hashキー : リポジトリ名
KEY_REPOSITORY = :repository.freeze
# Hashキー : 作者名
KEY_AUTHOR = :author.freeze
# Hashキー : readme
KEY_README = :readme.freeze

# テンプレートファイル名
TEMPLATE_FILE = 'trends.html.erb'.freeze
# 作成htmlドキュメントファイル名
OUTPUT_FILE = 'trends.html'.freeze

# 送信先メールアドレス(Kindle端末)
SEND_TO = 'XXXXXXX@kindle.com'.freeze
# 送信元メールアドレス(Gmail)
GMAIL_ADR = 'XXXXXXX@gmail.com'.freeze
# Gmailアカウントのパスワード
PASSWORD = 'XXXXXXXXXXXX'.freeze
# メール件名
SUBJECT = 'Github trends for a week.'.freeze

# メール送信メソッド
def send_mail

  # Gmail用のsmtpオプション
  options = {
            :address              => "smtp.gmail.com",
            :port                 => 587,
            :domain               => "smtp.gmail.com",
            :user_name            => GMAIL_ADR,
            :password             => PASSWORD,
            :authentication       => :plain,
            :enable_starttls_auto => true  }

  mail = Mail.new
  mail.to = SEND_TO
  mail.from = GMAIL_ADR
  mail.subject = SUBJECT
  mail.delivery_method(:smtp, options)
  mail.add_file(OUTPUT_FILE)

  mail.deliver
end

# htmlドキュメント作成メソッド
def gen_html(trends)
  erb = ERB.new(File.read(TEMPLATE_FILE), nil, '-')
  ret = erb.result(binding)
  File.open(OUTPUT_FILE, "w:utf-8") do |f|
    f.write(ret)
  end
end

agent = Mechanize.new
agent.verify_mode = OpenSSL::SSL::VERIFY_NONE
page = agent.get('https://github.com/trending?since=weekly')

trends = []
page.search('h3 a').each do |link|
  trend = {}
  array = link.text.gsub(/(\s)/,"").split('/')
  # リポジトリ名の取得
  trend[KEY_REPOSITORY] = array[1]
  # 作者名の取得
  trend[KEY_AUTHOR] = array[0]

  # リポジトリのページへ遷移
  url = "#{GITHUB_TOP}#{link[:href]}"
  next_page = agent.get(url)
  # readmeの情報取得
  readme = next_page.at('#readme')
  trend[KEY_README] = readme
  # 配列に追加
  trends << trend
end

# html作成
gen_html(trends)
# メール送信
send_mail

2016年4月9日土曜日

MechanizeでWeb操作 2 - GithubのトレンドをKindleで読む -


KindleのPaperwhiteを購入した記事をちょっと前に書きましたが、
Kindleでは電子書籍以外にも気になったWebページをKindleで読めたりします

Amazonでログインして、アカウントサービス > コンテンツと端末の管理 > 
設定と進んでいくと、パーソナル・ドキュメント設定という項目に
自分がkindleを使っている各端末にメールアドレスが割り振られています。

このメールアドレスに以下のファイル形式でドキュメントを添付して送れば
Kindle端末で読めるようになります。
  • Kindle形式(.MOBI、.AZW)
  • Microsoft Word(.DOC、.DOCX)
  • HTML(.HTML、.HTM)
  • RTF(.RTF)
  • Text(.TXT)
  • JPEG(.JPEG、.JPG)
  • GIF(.GIF)
  • PNG(.PNG)
  • BMP(.BMP)
  • PDF(PDF)
これを利用して、Githubのトレンド(1週間分)をKindleで読むようにしたいと思います。

MechanizeでGithubのトレンドを収集

インストールや環境は前回の記事を参照
まずはGithubのトレンドのページの情報を取得してみます。
require 'mechanize'

GITHUB_TOP = 'https://github.com'.freeze

agent = Mechanize.new
agent.verify_mode = OpenSSL::SSL::VERIFY_NONE
page = agent.get('https://github.com/trending?since=weekly') # (1)
page.search('h3 a').each do |link| # (2)
  url = "#{GITHUB_TOP}#{link[:href]}" # (3)
  next_page = agent.get(url)
  puts next_page.title # (4)
end
やってる事は、
(1) Githubのトレンド(1週間分)のページを取得
(2) <h3>タグ内の<a>リンク情報を取得
(3) リンクのURLを取得し完全なURLを作成
(4) リンク先のページを取得しタイトルを表示しています。
↑を実行するとリポジトリのタイトル一覧が表示されるかと思います。
ただKindleに送る情報としては、リポジトリと作者名、READMEの情報を
送ろうと思っているので少し修正します。
require 'mechanize'

GITHUB_TOP = 'https://github.com'.freeze

agent = Mechanize.new
agent.verify_mode = OpenSSL::SSL::VERIFY_NONE
page = agent.get('https://github.com/trending?since=weekly')
page.search('h3 a').each do |link|
  # Delete space and line code.
  array = link.text.gsub(/(\s)/,"").split('/')
  # Get repository name.
  puts "repository : #{array[1]}" # (5)
  # Get author name.
  puts "author : #{array[0]}" # (6)
  url = "#{GITHUB_TOP}#{link[:href]}"
  next_page = agent.get(url)
  readme = next_page.at('#readme')
  puts readme.text # (7)
end
一応これでリポジトリ名(5)、作者名(6)、README(7)を取得することが
できます。
実行する際は
$ bundle exec ruby main.rb
で実行できます。実行するとずらずらとコンソールに出力されるかと思います。
次回はそれをKindle端末にメール送信してみたいと思います。