<-- Home

Ruby胶水


胶水语言,调用别的语言编写的功能模块,将它们有机的结合在一起形成更高效的新程序。

这几天研究了一下如何用Ruby调用C/C++语言的DLL,主要是用到了Ruby FFI这个Gem。 Ruby做为一种最魔幻、完全面向对象、是程序员最好的朋友的语言,却经常被人吐槽性能。我就纳闷了,你们开发效率那么低,项目都做不出来,却硬要跟Ruby比性能?

好好好,我承认Ruby的性能的确不是让人很满意,特别是Ruby在Win环境下的虚拟机,体验太差。

如果我能发挥Ruby的胶水语言特性,使用Ruby调用C语言编写的DLL。需要大量计算的功能由C实现,在Ruby中调用,这样不是既能发挥Ruby优雅的特点,也能兼有C的性能了?

我在Win环境下做了实验,将C/C++程序编译成DLL,然后用Ruby调用。

Win下面的编译工具我有两种,一种是VS2015,另一种是MingGW。对于VS,这个宇宙级IDE,编译Win32 DLL再简单不过了,但我更喜欢用Code::Blocks这个IDE写C/C++,于是我决定用MinGW来编译C/C++程序。

先写了个简单的测试程序

#include <stdio.h>
extern "C" __declspec(dllexport) void __stdcall test();

void __stdcall test()
{
    printf("hello world!");
}

这里的extern "C"是让编译器将这个函数按C语言的方式进行编译,__declspec(dllexport)是DLL导出函数修饰符,__stdcall是规定函数的压栈方式。

然后使用gcc编译成DLL

gcc -shared test.cpp -o test.dll

编写Ruby脚本

require 'ffi'

module DLLTest
    extend FFI::Library
    ffi_lib 'test'
    ffi_convention :stdcall

    attach_function :test, "test", [], :void
end

DLLTest.test

ffi_lib导入刚刚编译好的DLL,用attach_function导入DLL里面的函数到DLLTest模块里,:test是导入后模块的方法名,"test"是DLL中的函数名,数组是函数的形参表,没有就是空数组,:void是DLL中的函数返回类型

然后运行

D:\>ruby test.rb
hello world!

胶水功能就实现了! 具体用法可以去Ruby FFi Wiki查看!

然后让我们搞点事情! 比如说用Ruby调用SDL2库画图,有个rubysdl的Gem但是效率太低,我们尝试调用SDL2.dll来画图。

先编写cpp

// g++ -shared ffisdl.cpp -o ffisdl.dll -lSDL2 -lSDL2main
#include<SDL2/SDL.h>

extern "C" __declspec(dllexport) void __stdcall Init();
extern "C" __declspec(dllexport) int __stdcall CreateWindow(char* title, Uint32 x, Uint32 y, Uint32 w, Uint32 h);
extern "C" __declspec(dllexport) int __stdcall CreateRenderer();
extern "C" __declspec(dllexport) void __stdcall Delay(Uint32 time);
extern "C" __declspec(dllexport) int __stdcall HandleEvent();
extern "C" __declspec(dllexport) int __stdcall SetRenderDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a);
extern "C" __declspec(dllexport) void __stdcall RenderPresent();
extern "C" __declspec(dllexport) int __stdcall RenderClear();
extern "C" __declspec(dllexport) int __stdcall RenderDrawPoint(int x,int y);
extern "C" __declspec(dllexport) int __stdcall RenderDrawLines(SDL_Point* points,int count);
extern "C" __declspec(dllexport) SDL_Point* __stdcall CreatePoints(int n);

static SDL_Window* win = NULL;
static SDL_Renderer* renderer = NULL;

void __stdcall Init()
{
    SDL_Init(SDL_INIT_EVERYTHING);
}

int __stdcall  CreateWindow(char* title, Uint32 x, Uint32 y, Uint32 w, Uint32 h)
{
	win = SDL_CreateWindow(title, x, y, w, h, SDL_WINDOW_SHOWN);
	if(win == NULL)
        return 1;
    else
        return 0;
}

int __stdcall CreateRenderer()
{
    renderer = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
    if(renderer == NULL)
        return 1;
    else
        return 0;
}

void __stdcall Delay(Uint32 time)
{
    SDL_Delay(time);
}

int __stdcall HandleEvent()
{
    SDL_Event e;
    while(SDL_PollEvent(&e))
    {
        switch(e.type)
        {
        case SDL_QUIT:
            return 1;
        }
    }
    return 0;
}

int __stdcall SetRenderDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a)
{
    return SDL_SetRenderDrawColor(renderer, r, g, b, a);
}

int __stdcall RenderClear()
{
    return SDL_RenderClear(renderer);
}

void __stdcall RenderPresent()
{
    SDL_RenderPresent(renderer);
}

int __stdcall RenderDrawPoint(int x, int y)
{
    return SDL_RenderDrawPoint(renderer, x, y);
}

int __stdcall RenderDrawLines(SDL_Point* points,int count)
{
    return SDL_RenderDrawLines(renderer, points, count);
}

SDL_Point* __stdcall CreatePoints(int n)
{
    return new SDL_Point[n];
}

编译

g++ -shared ffisdl.cpp -o ffisdl.dll -lSDL2 -lSDL2main

我们已经将需要的功能封装好,接着用“胶水”将Ruby和DLL连接起来。这次是画一个圆!

require 'ffi'
module SDL
  extend FFI::Library
  ffi_lib 'SDL2'
  ffi_lib 'ffisdl'
  ffi_convention :stdcall
  class Point < FFI::Struct
    layout :x,  :int,
           :y,  :int
  end
  attach_function :init, "Init", [], :void
  attach_function :createWindow, "CreateWindow",[:string, :uint32, :uint32, :uint32, :uint32], :int
  attach_function :createRenderer, "CreateRenderer",[], :int
  attach_function :delay, "Delay", [:uint32], :void
  attach_function :handleEvent, "HandleEvent", [], :int
  attach_function :setRenderDrawColor, "SetRenderDrawColor", [:uint8, :uint8, :uint8, :uint8], :int
  attach_function :renderClear, "RenderClear", [], :int
  attach_function :renderPresent, "RenderPresent", [], :void
  attach_function :renderDrawPoint, "RenderDrawPoint", [:int, :int], :int
  attach_function :renderDrawLines, 'RenderDrawLines', [:pointer, :int ], :int
end


class Round
  def initialize(x, y, r, s)
    @x = 800 * x / 100
    @y = 600 * y / 100
    @r = 800 * r / 100
    @val_array = FFI::MemoryPointer.new(SDL::Point, s + 2)
    points = []
    (s+2).times {|i| points << SDL::Point.new(@val_array[i])}
    o = 360.0 / s
    for i in (0...s+2) do
      points[i][:x] = @x - @r * (Math.cos(o * i * 3.1415926 / 180))
      points[i][:y] = @y - @r * (Math.sin(o * i * 3.1415926 / 180))
    end
  end
  def getPoints
    @val_array
  end
end


round = Round.new(50, 50, 20, 100)

SDL.init
SDL.createWindow "Window Created by Ruby ffi", 200, 100, 800, 600
SDL.createRenderer
quit = 0;

while quit != 1
  SDL.setRenderDrawColor 255, 255, 255, 0
  SDL.renderClear 
  SDL.setRenderDrawColor 0, 0, 0, 0
  SDL.renderDrawLines round.getPoints, 102
  quit = SDL.handleEvent
  SDL.renderPresent
  SDL.delay(33)
end

运行脚本

D:>ruby test.rb

听说Ruby没有很好的播放mp3的Gem,那自己写一个,调用C的! 用C的libmad解码mp3,用SDL2播放PCM音频

// g++ -shared ffisdl.cpp -o ffisdl.dll -lSDL2 -lSDL2main -lmad

#include <SDL2/SDL.h>
#include <mad.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>

void __stdcall Init()
{
    SDL_Init(SDL_INIT_EVERYTHING);
}

extern "C" __declspec(dllexport) void __stdcall PlayMp3Data(char*, int);

const double rate24_16 = 0x7fff / (double)0x7fffff;

void audio_play_callback(void *data, Uint8* stream, int len)
{
    void** in = (void**)data;
    mad_frame* mp3Frame = (mad_frame*)in[0];
    mad_stream* mp3Stream = (mad_stream*)in[1];
    mad_synth* mp3Synth = (mad_synth*)in[2];
    memset(stream, 0, len);
    mad_frame_decode(mp3Frame, mp3Stream);
    mad_synth_frame(mp3Synth, mp3Frame);
    int16_t* temp = (int16_t*)malloc(len);
    int16_t* p = temp;

    for (int i = 0; i < 1152; ++i)
    {
        *p++ = (mp3Synth->pcm.samples[0][i] >> 7) * rate24_16 * 3;
        *p++ = (mp3Synth->pcm.samples[1][i] >> 7) * rate24_16 * 3;
    }
    SDL_MixAudio(stream, (uint8_t *) temp, len, SDL_MIX_MAXVOLUME);
    free(temp);
}
unsigned char* ReadMp3File(const char* file, int& size,int headlen)
{
    FILE* fp = fopen(file, "rb");
    if(fp == NULL)
        return 0;
    fseek(fp, 0, SEEK_END);
    size = ftell(fp) - headlen;
    fseek(fp, headlen, SEEK_SET);
    unsigned char* data = (unsigned char*)malloc(size);
    if(data == NULL)
        return 0;
    fread(data, 1, size, fp);
    fclose(fp);
    return data;
}


void __stdcall PlayMp3Data(char* file, int headlen)
{
    SDL_AudioSpec want;
    mad_header mp3Header;
    mad_synth* mp3Synth = new mad_synth;
    mad_stream* mp3Stream = new mad_stream;
    mad_frame* mp3Frame = new mad_frame;
    int size;
    unsigned char* data = ReadMp3File(file, size, headlen);
    if(data == NULL)
        return;
    mad_stream_buffer(mp3Stream, data, size);
    mad_header_decode(&mp3Header, mp3Stream);
    want.freq = mp3Header.samplerate;
    want.format = AUDIO_S16SYS;
    want.channels = 2;
    want.samples = 1152;
    void* in[] = {mp3Frame, mp3Stream, mp3Synth};
    want.userdata = in;
    want.callback = audio_play_callback;
    mad_header_finish(&mp3Header);
    SDL_OpenAudio(&want,NULL);
    int flag = 0;
    SDL_PauseAudio(flag);
    getchar();//这里偷懒了哦
    SDL_CloseAudio();
    mad_stream_finish(mp3Stream);
    mad_frame_finish(mp3Frame);
    mad_synth_finish(mp3Synth);
    delete mp3Stream;
    delete mp3Synth;
    delete mp3Frame;
}

编译

g++ -shared ffisdl.cpp -o ffisdl.dll -lSDL2 -lSDL2main -lmad

运行脚本

require 'mp3info'
require 'ffi'

mp3headlen = 0
title = ""
album = ""
artist = ""

Mp3Info.open($*[0]) do |mp3|
  if mp3.hastag2?
    title = mp3.tag2["TIT2"]
    album =  mp3.tag2["TALB"]
    artist =  mp3.tag2["TPE1"]
    mp3headlen = mp3.tag2.io_position
  end
end

module SDL
  extend FFI::Library
  ffi_lib 'SDL2'
  ffi_lib 'libmad-0'
  ffi_lib 'ffisdl'
  
  ffi_convention :stdcall
  attach_function :playMp3Data, "PlayMp3Data", [:string, :int], :void
end

puts "歌曲信息"
puts "曲名:#{title}"
puts "专辑:#{album}"
puts "艺术家:#{artist}"
SDL.playMp3Data($*[0],mp3headlen)

由于libmad不解码ID3V2头,所以要跳过。

D:\> ruby test.rb "D:\CloudMusic\senya - 色は匂へど散りぬるを (Vocal version).mp3"
歌曲信息
曲名:色は匂へど散りぬるを (Vocal version)
专辑:第1幕 協奏曲「色は匂へど散りぬるを」 SIDE A
艺术家:senya

然后就听见音乐啦。

可能你会觉得这么做还不如直接用C做,但是我觉得以后大工程肯定不会只是用一种语言开发的,胶水这种功能肯定会用上。