User Tools

Site Tools


inf:ruby:rozdz_11

Dynamiczne wykonywanie kodu

Jako dynamiczny, interpretowany jezyk, Ruby umozliwia uruchamianie kodu generowanego dynamicznie. Robi sie to za pomoca metody eval.

  • eval “puts 2 + 2” wyswietli 4 ale zwroci nil, bo to jest zawsze zwracane przez metode puts,
  • puts eval(“2 + 2”) zwroci 4.

Bindings

W Rubym binding to referencja do jakiegos kontekstu, zakresu lub stanu wykonania.

# bindings.rb
 
def binding_elsewhere
    x = 20
    return binding
end
 
remote_binding = binding_elsewhere
 
x = 10
eval("puts x")
eval("puts x", remote_binding)

eval akceptuje drugi parametr, wlasnie te bindingi. W powyzszym przypadku, ten binding zwracany jest z metody binding_elsewhere. Zmienna remote_binding zawiera referencje do kontekstu wykonania wewnatrz binding_elsewhere, a nie w glownym kodzie. Dlatego wyswietlajac x, 20 jest pokazywane, poniewaz tyle wlasnie wynosi ono wewnatrz binding_elsewhere.

Inne formy eval'a

class_eval

class_eval idealny jest do dynamicznego dodawania metod do klasy:

# class_eval.rb
 
class Person
end
 
def add_accessor_to_person(accessor_name)
    Person.class_eval %Q{
        attr_accessor :#{accessor_name}
    }
end
 
person = Person.new
add_accessor_to_person :name
add_accessor_to_person :gender
 
person.name = "Peter Cooper"
person.gender = "male"
 
puts "#{person.name} is #{person.gender}"

Korzystamy z metody add_accessor_to_person aby dynamicznie dodac akcesory do klasy Person. Innym rozwiazaniem jest rozszerzenie klasy Class o metode dodajaca akcesory:

# class_eval_2.rb
 
class Class
    def add_accessor(accessor_name)
        self.class_eval %Q{
            attr_accessor :#{accessor_name}
        }
    end
end
 
class Person
end
 
person = Person.new
Person.add_accessor :name
Person.add_accessor :gender
person.name = "Petr Cooper"
person.gender = "male"
 
puts "#{person.name} is #{person.gender}"

instance_eval

instance_eval to cos jak zwykly eval ale w kontekscie obiektu (a nie metody):

# instance_eval.rb
 
class MyClass
    def initialize
        @my_variable = 'Hello, world!'
    end
end
 
obj = MyClass.new
obj.instance_eval { puts @my_variable }

Tworzenie wlasnej wersji attr_accessor'a

# own_attr_acc.rb
 
class Class
    def add_accessor(accessor_name)
        self.class_eval %Q{
            def #{accessor_name}
                @#{accessor_name}
            end
 
            def #{accessor_name}=(value)
                @#{accessor_name} = value
            end
        }
    end
end

Powyzsze to odpowiednik standardowego akcesora:

def name
    @name
end
 
def name=(value)
    @name = value
end

Uruchamianie innych programow z Rubego

Otrzymywanie wynikow z innych programow

Mamy trzy rozne metody uruchamiania zewnetrznych programow z poziomu Rubego (podobnie jak w Perlu):

  • metoda system modulu Kernel - idealna, gdy nie interesuje nas to co wyswietli program,
  • za pomoca tzw. backtickow - `` - gdy potrzebujemy tego co program wyswietla,
  • za pomoca delimited input literals - %x{} - funkcjonalnie to odpowiednik backtickow.

Przekazywanie wykonywania do innego programu

Aby zakonczyc wykonywanie obecnego programu i uruchomic inny, skorzystac mozna z metody exec.

exec "ruby another_script.rb"
puts "To nigdy nie zostanie wyswietlone."

Uruchamianie dwoch programow jednoczesnie

Forking ma miejsce, gdy proces duplikuje sie, w wyniku czego powstaja dwa procesy programu dzialajace jednoczesnie. W Rubym mamy metode fork modulu Kernel, ktora tworzy taki fork. Metoda zwraca PID utworzonego procesu w glownym procesie a nil w procesie utworzonym (child). Forkowanie nie jest mozliwe w wersji Rubego na Windowsa.

# forking.rb
 
if fork.nil?
    exec "ruby some_other_file.rb"
end
 
puts "This script now continues to run alongside some_other_file.rb"

Jesli uruchamiany przez exec program ma sie kiedys zakonczyc i chcemy na to poczekac, musimy skorzystac z Process.wait.

# waiting.rb
 
child = fork do
    sleep 3
    puts "Child says 'Haizz'!"
end
 
puts "Waiting for the child process..."
Process.wait child
puts "All done!"

Interakcja z innym programem

Dzieki modulowi IO Rubego i jego metodzie popen mamy mozliwosc utworzenia strumienia I/O pomiedzy programami. Dziala to tylko w wypadku, gdy programy akceptuja bezposrednie wejscie i produkuja bezposrednie wyjscie w linii polecen.

# iols.rb
 
ls = IO.popen("ls", "r")
while line = ls.gets
    puts line
end
ls.close

Powyzej, otwieramy strumien pomiedzy naszym programem a linuksowym ls. Wczytujemy przez niego, linia po linii, nazwy plikow w katalogu.

Bezpieczne obchodzenie sie z danymi, niebezpieczne metody

Aplikacje czesto pracuja w srodowiskach i sytuacjach, w ktorych nie mozna zagwarantowac poprawnosci danych wejsciowych. Konieczne jest zabezpieczanie sie przeciw takim sytuacjom.

Skazone dane i obiekty

Wszystkie dane, ktore pochodza z zewnetrznych zrodel, lub takich zrodel, ktorych Ruby nie potrafi okreslic czy sa bezpieczne, uznawane sa jako skazone. Dane pochodzace z plikow, czy z linii polecen uznawane sa jako skazone. Dane zapisane w programie w postaci literalow uznawane sa jako nieskazone.

while x = gets
    puts "=> #{eval(x)}"
end

Przyklad wydaje sie niegrozny, ale wystarczy wpisac powyzej trwozace krew w zylach polecenie rm -rf /* i caly swiat ulegnie zniszczeniu.

Mozna sprawdzic, czy obiekt nie jest skazony za pomoca metody tainted?. Mozna zmienic obiekt ze skazonego na nieskazony za pomoca metody untaint.

# untaint.rb
 
def code_is_safe?(code)
    code =~ /[`;*-]/ ? false : true
end
 
while x = gets
    x.untaint if code_is_safe?(x)
    next if x.tained?
    puts "=> #{eval(x)}"
end

Poprawiony przyklad. Dokonujemy (bardzo podstawowego) sprawdzenia za pomoca code_is_safe? i na tej podstawie odkazamy wpisane polecenie umozliwiajac jednoczesnie jego uruchomienie za pomoca eval. next przeskakuje do nastepnej iteracji najbardziej wewnetrznej petli.

Poziomy bezpieczenstwa

Duzo silniejsza forma zabezpieczen sa poziomy bezpieczenstwa (safe levels). Poziomy te okreslaja, jakie funkcje Ruby udostepni i jak powinien radzic sobie ze skazonymi danymi. Funkcjonalnosc ta nie jest czesto uzywana, poniewaz utrudnia dzialanie bibliotek oraz poniewaz wiekszosc developerow woli pisac kod, ktory jest bezpieczny bez koniecznosci stosowania tych technik.

Aktualnie ustawiony poziom mozna sprawdzic za pomoca zmiennej $SAFE. Standardowo jest to poziom 0, najnizszy.

$SAFE Opis
0 Brak ograniczen.
1 Potencjalnie niebezpieczne metody nie moga korzystac ze skazonych danych. Obecny katalog nie jest dodawany do sciezek, z ktorych Ruby laduje biblioteki
2 FIXME
3 FIXME
4 FIXME

Watki

threads = []
 
10.times do
    thread = Thread.new do
        10.times { |i| print i; $stdout.flush; sleep rand(2) }
    end
 
    threads << thread
end
 
threads.each { |thread| thread.join }

Output:

0000000001111220345123111122312326454223423334376555344544585756966678664568797797767788899888899999

Tworzymy tablice do przechowywania watkow, aby miec latwy do nich dostep. Nastepnie tworzymy dziesiec watkow, wysylajac blok kodu do uruchomienia do metody Thread.new, jednoczesnie dodajac je do tablicy. Po utworzeniu watkow, czekamy, az wszystkie ukoncza dzialanie. Robimy to za pomoca metody join kazdego watku, ktora powoduje, ze glowny program czeka az watki zakoncza dzialanie zanim wznowi swoje wlasne.

Zaawansowane operacje z watkami

Metoda join przyjmuje parametr, za pomoca ktorego mozemy okreslic timeout, po ktorym metoda join zwroci nil.

threads.each do |thread|
    puts "Thread #{thread.object_id} didn't finish in 1s" unless thread.join(1)
end

Metoda Thread.list zwraca globalna liste wszystkich watkow. Warto jednak zawsze miec swoja wlasna, jesli w programie korzysta sie zwiecej niz jednej grupy watkow i chcemy trzymac je osobno do uzycia z join lub innymi funkcjami.

# threads_02.rb
 
10.times { Thread.new { 10.times { |i| print i; $stdout.flush; sleep rand(2) } } }
Thread.list.each { |thread| thread.join unless thread == Thread.main }

Lista zwracana metoda list zawiera rowniez watek glowny, musimy zatem wykonac sprawdzenie.

Watki potrafia komunikowac sie z schedulerem watkow i dostarczac informacji o ich statusie. Watek moze sie sam zatrzymac, za pomoca metody Thread.stop. Mozna go wznowic lub zrestartowac z programu glownego za pomoca metody run.

Thread.list.each { |thread| thread.run }

Watek moze rowniez powiedziec schedulerowi, ze chce przekazac wykonywanie do innego watku. Taka technika zwana jest cooperative multitasking. Poprawnie wykorzystana, potrafi zwiekszyc wydajnosc wielowatkowosci.

# thread_stop.rb
 
2.times { Thread.new { 10.times { |i| print i; $stdout.flush; Thread.pass } } }
Thread.list.each { |thread| thread.join unless thread == Thread.main }

Output:

01234567890123456789

Wlokna (fibers)

Wlokna to alternatywa dla watkow w Rubym 1.9 i nowszych. Wlokna to lekkie jednostki wykonywania, ktore kontroluja swoj wlasny scheduler (cooperative scheduling). Podczas gdy watki zazwyczaj dzialaja ciagle, wlokna, po wykonaniu zadania, przekazuja kontrole schedulerowi, to znaczy istnieja dalej i moga byc wznawiane.

# fiber.rb
 
sg = Fiber.new do
    s = 0
    loop do
        square = s * s
        Fiber.yield square
        s += 1
    end
end
 
10.times { puts sg.resume }

Wlokno bedzie dzialac, dopoki nie wywolamy Fiber.yield, aby oddac kontrole, temu kto ostatni kazal wloknu sie uruchomic (w tym przypadku metodzie sg.resume). Pomimo tego, ze wlokno posiada nieskonczona petle, nie dziala ono caly czas, nie powodujac spadku wydajnosci. Jesli celowo damy wloknu jakis punkt jego zakonczenia, przywolanie jego metody resume spowoduje wyjatek.

Przekazywanie danych do wlokna

# fiber_data.rb
 
sg = Fiber.new do
    s = 0
    loop do
        square = s * s
        s += 1
        s = Fiber.yield(square) || s
    end
end
 
puts sg.resume
puts sg.resume
puts sg.resume
puts sg.resume
puts sg.resume 40
puts sg.resume
puts sg.resume
puts sg.resume 0
puts sg.resume
puts sg.resume

Output:

0
1
4
9
1600
1681
1764
0
1
4

W piatej probie, przekazujemy liczbe 40, ktora jest przypisywana zmiennej s wlokna. Kilka iteracji pozniej, zerujemy s. Wartosc jest odbierana przez wlokno w wyniku wywolania Fiber.yield.

Dlaczego wlokna?

  • W niektorych sytuacjach wieksza wydajnosc. Utworzenie setek wlokien jest *duzo* szybsze niz watkow, w szczegolnosci w przypadku Rubego 1.9, gdzie watki tworzone sa na poziomie systemu operacyjnego. Wieksza jest rowniez wydajnosc pamieciowa.
  • Mozemy uruchomic tylko jedno wlokno w danym czasie (w jednym watku) i my musimy zajac sie schedulingiem, co w niektorych sytuacjach moze okazac sie zaleta.

RubyInline

Jako jezyk wysokiego poziomu, zorientowany obiektowo, Ruby nie zostal zaprojektowany do sytuacji wymagajacych wysokiej wydajnosci w tradycyjnym tego slowa znaczeniu. Obecnie, obliczenia nie sa wymagajace, niemniej zdazaja sie sytuacje, gdzie wysoka wydajnosc jest niezbedna.

Wtedy dobrym pomyslem moze byc napisanie tych czesci programu w innym jezyku i uruchomic taki kod z Rubego. Istnieje biblioteka, RubyInline, ktora to umozliwia. Najczesciej wykorzystywana jest do pisania w C lub Cpp.

Idealnym przykladem ilustrujacym RubyInline i potege C moze byc utworzenie podstawowej metody (funkcji w nomenklaturze C) aby obliczac silnie.

# rubyoffline.rb
 
class Fixnum
    def factorial
        (1 .. self).inject { |a, b| a * b }
    end
end
 
require 'benchmark'
 
Benchmark.bm do |bm|
    bm.report('ruby:') do
        100000.times do
            8.factorial
        end
    end
end

Output:

      user     system      total        real
ruby:  0.190000   0.010000   0.200000 (  0.200773)

inject: http://www.ruby-doc.org/core/classes/Enumerable.html#M001494

A teraz z wykorzystaniem C. Musialem wprowadzic pare zmian, bo oryginalny przyklad z ksiazki nie dzialal. Zakomentowalem utworzenie singletona z tego obiektu (aby nie musiec go tworzyc) class « self. Dodalem tworzenie instancji cfact = CFactorialnew. http://www.sitepoint.com/forums/ruby-227/rubyinline-errors-calling-static-inline-methods-621556.html

# rubyinline.rb
 
require 'rubygems'
require 'inline'
require 'benchmark'
 
class CFactorial
#    class << self
        inline do |builder|
            builder.c %q{
                long factorial(int value) {
                    long result = 1, i = 1;
                    for(i = 1; i <= value; i++) {
                        result *= i;
                    }
                    return result;
                }
            }
        end
# end
end
 
cfact = CFactorial.new
 
Benchmark.bm do |bm|
    bm.report('c:') do
        100000.times { cfact.factorial(8) }
    end
end

Output:

      user     system      total        real
c:  0.010000   0.000000   0.010000 (  0.016394)

Roznica jest dramatycznie duza.

Unicode, kodowanie znakow, wsparcie dla UTF-8

Unicode to standard przedstawienia znakow kazdego pisma na swiecie. To jedyny sposob aby zarzadzac wieloma roznymi alfabetami i zestawami znakow.

Wiecej: http://www.joelonsoftware.com/articles/Unicode.html

Od wersji 1.9, Ruby obsluguje znaczna liczbe kodowan (95). Wsparcie istnieje nie tylko dla lancuchow znakow ale rowniez i dla samego kodu zrodlowego!

ruby-1.9.2-p180 :001 > Encoding.list.join(" ")
 => "ASCII-8BIT UTF-8 US-ASCII Big5 Big5-HKSCS Big5-UAO CP949 Emacs-Mule EUC-JP EUC-KR EUC-TW GB18030 GBK ISO-8859-1 ISO-8859-2 ISO-8859-3 ISO-8859-4 ISO-8859-5 ISO-8859-6 ISO-8859-7 ISO-8859-8 ISO-8859-9 ISO-8859-10 ISO-8859-11 ISO-8859-13 ISO-8859-14 ISO-8859-15 ISO-8859-16 KOI8-R KOI8-U Shift_JIS UTF-16BE UTF-16LE UTF-32BE UTF-32LE Windows-1251 IBM437 IBM737 IBM775 CP850 IBM852 CP852 IBM855 CP855 IBM857 IBM860 IBM861 IBM862 IBM863 IBM864 IBM865 IBM866 IBM869 Windows-1258 GB1988 macCentEuro macCroatian macCyrillic macGreek macIceland macRoman macRomania macThai macTurkish macUkraine stateless-ISO-2022-JP eucJP-ms CP51932 GB2312 GB12345 ISO-2022-JP ISO-2022-JP-2 CP50220 CP50221 Windows-1252 Windows-1250 Windows-1256 Windows-1253 Windows-1255 Windows-1254 TIS-620 Windows-874 Windows-1257 Windows-31J MacJapanese UTF-7 UTF8-MAC UTF8-DoCoMo SJIS-DoCoMo UTF8-KDDI SJIS-KDDI ISO-2022-JP-KDDI stateless-ISO-2022-JP-KDDI UTF8-SoftBank SJIS-SoftBank"

Aby sprawdzic kodowanie danego lancucha, korzystamy z metody encoding:

ruby-1.9.2-p180 :003 > "this is a test".encoding
 => #<Encoding:UTF-8> 

Aby przekonwertowac lancuch na inne kodowanie, korzystamy z metody encode:

ruby-1.9.2-p180 :005 > "this is a test".encode("ISO-8859-2").encoding
 => #<Encoding:ISO-8859-2>

Aby wlaczyc wsparcie kodowania znakow w samym kodzie zrodlowym, nalezy pierwsza (lub druga, jesli korzystamy z linii shebang) linie wypelnic tak:

# coding: utf-8
inf/ruby/rozdz_11.txt · Last modified: 2021/02/16 09:56 (external edit)