App by webrick

ruby webrick

前回はOpenBSDに備付のWEBサーバーでCGIをやってみた。じゃ、OpenBSD以外のOSはどうするん?

普通に考えたらapacheを入れる。そんなの大袈裟すぎる。ひょんな亊からgolangに備付のhttpの解説に行き当ってしまっていた(前回の最後の部分)。で、目覚めた訳よ。

WEBサーバーと構える必要が全く無いって亊。ブラウザーからのリクエストを受け付けて、それに返事出来れば、りっぱなWEBサーバーだって亊。

じゃ、golangでいいじゃんとも思ってしまうんだけど、コンパイラーだからなあ。たまには、軽くて便利なインタープリタに触ってみるか。猫の杓子もpythonは止めておいてrubyを登場してもらいます。

備付のWEBサーバー有ったよな。ええとwebrickってやつ。標準添付品のはずなんだけど、ruby 3.0になった時、分家に出されてしまったようだ。もし入っていなかった、gemから入れるべし。

ob$ doas gem install webrick
Fetching webrick-1.7.0.gem
Successfully installed webrick-1.7.0
Parsing documentation for webrick-1.7.0
Installing ri documentation for webrick-1.7.0
Done installing documentation for webrick after 2 seconds
1 gem installed

何で分家に出された? お家騒動ってか内紛があったのだろう。ruby3になって、性能が劇的にあがった。matz曰く 3 X 3 って亊らしい。ruby3になると3倍速くなります。webrickは、作りが古い為、この恩恵に与れない。ずっと居座っていると看板の足を引っ張る亊になる。なら、本体から外して分家に出しちゃ。これ、オイラーの勝手な邪推。どうなんでしょ?

ob$ ruby -run -e httpd .
[2022-04-12 06:38:35] INFO  WEBrick 1.7.0
[2022-04-12 06:38:35] INFO  ruby 3.0.3 (2021-11-24) [x86_64-openbsd]
[2022-04-12 06:38:35] INFO  WEBrick::HTTPServer#start: pid=60970 port=8080

un.rb って言うunixもどきの真似をさせるモジュールにより、上記のように起動出来る。 カレント・ディレクトリィーがhtdocs兼cgi-binになる。ようするになんでもござれ状態だ。

少し資料を探っておく。

library webrick

library webrick/cgi

Ruby 標準ライブラリの WEBrick で Web サーバを作る for monitor

WEBrickを使って動的なhtmlを返す(〜 GETとPOSTの違い 〜)

ruby sqlite3

rubyからもsqlite3を使えるようにする。 リナの場合、ヘッダー無いぞのイジメに会わないように、防御してからgemする。

sudo apt install libsqlite3-dev ; sudo gem install sqlite3

前回書いたrustのやつと似せてみた。

require 'sqlite3'

def search(sw)
  ew = sw + "%"
  db = SQLite3::Database.new("/usr/share/dict/ejdict.sqlite3")
  stmt = db.prepare("SELECT word,mean FROM items WHERE word like ? limit 3")
  stmt.bind_param(1, ew)
  stmt.execute().each do |res|
    puts("<dt>#{res[0]}</dt>\n")
    puts("<dd>#{res[1]}</dd>\n")
  end
  stmt.close(); db.close()
end

puts("Content-type: text/html; charset=UTF-8\n")
search("clap")

辞書は、 無料 英和辞書データ ダウンロード から頂いたものを、単語帳が有る場所に保存しておいた。 こんな有用な辞書を公開下さって、感謝です。 m(_ _)m

small server

app.rb

require 'webrick'
srv = WEBrick::HTTPServer.new({ :DocumentRoot => './',
                                :BindAddress => '0.0.0.0',
                                :Port => 8080})
srv.mount('/view.cgi', WEBrick::HTTPServlet::CGIHandler, 'view.rb')
srv.mount('/foo.html', WEBrick::HTTPServlet::FileHandler, 'hoge.html')
trap("INT"){ srv.shutdown }
srv.start

view.rb

#!/usr/local/bin/ruby
require 'cgi'
cgi = CGI.new
html = <<-EOF
<html><head><title>cgi test</title></head><body>
<h1>Hello</h1>
</body></html>
EOF
puts cgi.header
puts html

hoge.html

<html><head><title>test html</title></head><body>
<h1>Test html</h1>
</body></html>

BindAddressは何処からでもかかってきなさいというpython仕様に合わせてあるんで、良い子は注意してね。http://localhost:8080/foo.html とかすると、アクセス出来るよ。

古めかしいHTTPServletなんてクラス名が使われてるのが分家された一因なのだろうか。今時Javaと一緒にして貰いたくはないわって、いきまいている人が居るんだな。 Java Servlet

cgi化

これで役者は揃った。早速webrickなサーバーを介して、世界の標準入出力装置であるブラウザーから、辞書を引けるようにしてみよう。

sql.rbをcgi対応にする。ここで登場するのは、cgiモジュールだ。懐しいこったい。

#!/usr/bin/env ruby
require 'cgi'
  :
cgi = CGI.new()
ew = cgi['ew']
puts( cgi.header(options = "text/html; charset=UTF-8") )
puts("<html><head><title>search</title></head><body>\n")
search(ew)
puts("</body></html>\n")

CGI.new("html3.0")とか指定出来るけど、なくても普通は困らない。これって、権威の象徴W3Cと現場の綱引きの象徴だからね。下手に指定すると、災難が降り掛かるぞ。詳細は、 どうしてHTML5が廃止されたのか を参照。

cgi用のスクリプトは、オフラインで確認出来るように配慮されてる(by cgi/core.rb)。

sakae@pen:/tmp/t$ ./sql.rb
(offline mode: enter name=value pairs on standard input)
ew=ruby                                   # Ctl-d to continue
Content-Type: text/html; charset=UTF-8

<html><head><title>search</title></head><body>
<dt>ruby</dt>
<dd>〈C〉紅玉,ルビー / 〈U〉ルビー色,深紅色(しんこうしょく) / ルビー色の,深紅の,
日本発のプログラミング言語</dd>
</body></html>
[2022-04-13 06:45:24] ERROR CGIHandler: sql.rb:\nenv: ruby: No such file or directory\n
[2022-04-13 06:45:24] ERROR CGIHandler: sql.rb exit with 127
[2022-04-13 06:45:24] ERROR Premature end of script headers: sql.rb
10.0.2.2 - - [13/Apr/2022:06:45:24 JST] "POST /ejdict HTTP/1.1" 500 317
http://localhost/ -> /ejdict

OpenBSD特有な亊かも知れないけど、シェバングに #!/usr/bin/env ruby とかやって、勝手に正しい位置を探して貰おうとすると、上記のエラーになった。手抜きをせず、絶対PATHで指定してあげよう。

app

webric/config.rb

:DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"]

matzの発明品、index.rhtmlなんてのも顔を出している。このセットは、URLでDIRを指定した時、そのDIR内にこれらのファイルがあると、それが使われますよって指定だ。apacheとかの有名なやつだと、これらの他にも有効になってるぞ。例えば、home.html とかね。

で、これを踏まえて、app.rbのルーティングを次のように指定した。

srv.mount('/ejdict', WEBrick::HTTPServlet::CGIHandler, 'sql.rb')
srv.mount('/', WEBrick::HTTPServlet::FileHandler, 'index.htm')

サーバーの公開エリアだどうなってるか、隠すんだな。対応するindex.htmは、

<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head><body>
English word to Japanese
<br>
<form method=POST action="/ejdict">
<input type="text" name="ew">
<br>
<input type="submit" value="trans">
</form></body></html>

これはユーザーに公開されるけど、これを見たって、内部で実行されるcgiスクリプトは判明しない。多少はセキュリティーに寄与するのかな。

single file

ここまでで、app.rb index.htm sql.rb と3つのファイルを用意すれば使えるようになった。 でも、これって取回しに不便。それにsqlite3な辞書にアクセスする度にrubyのプロセスが余分に走る。これは許せないぞ。

出来る亊なら、app.rb これ一つにまとめたい、と、竹村健一的な欲求が出て来た。歳がバレるな。

と言う亊で、index.htmとsql.rbをapp.rbに吸収したやつにしてみた。アプリが走ると今居るdirに問答無用でindex.htmが作成される。まあ、3文字のサフィックスはWindows屋さんが好んで使うんで許せ。それから、書込み出来る場所って亊が必要。/tmpがお勧め。勝手に作られたindex.htmは放置してるんで、これも許せ。これらに不満が有るなら、勝手に改造されたし。

で、問題が … VirtualBOX上のOpenBSD ruby 2.7.2p137 では、問題なく動くんだけど、Debianな環境では、検索出来無い。辞書引きを拒否しちゃうんだ。なんでかな?

# Usage: run this script and then firefox http://localhost:8080/
# warning index.htm file auto maked in current dir

require 'webrick'
require 'sqlite3'

KICK = 'index.htm'

def setup_file(kick)
  src = '<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head><body>
English word to Japanese
<br>
<form method=POST action="/ejdict">
<input type="text" name="ew">
<br>
<input type="submit" value="trans">
</form></body></html>
'
  File.write(kick, src)
end

def httpd(kick)
  srv = WEBrick::HTTPServer.new({ :DocumentRoot => './',
                                  :FancyIndexing => false,
                                  :BindAddress => '0.0.0.0',
                                  :Port => 8080})
  srv.mount('/', WEBrick::HTTPServlet::FileHandler, kick)

  srv.mount_proc("/ejdict") do |req, res|
    qry = req.query
    ew = qry['ew'] + "%"
    db = SQLite3::Database.new("/usr/share/dict/ejdict.sqlite3")
    stmt = db.prepare("SELECT word,mean FROM items WHERE word like ? limit 5")
    stmt.bind_param(1, ew)

    body = "<html><head><title>ejdict</title></head><body>\n"
    body << "<h2>SQL search #{ew} limit 5</h2>\n"
    stmt.execute().each do |rb|
      body << "<dt>#{rb[0]}</dt>\n"
      body << "<dd>#{rb[1]}</dd>\n"
    end
    stmt.close(); db.close()
    body << "</body></html>\n"

    res.status = 200
    res['Content-Type'] = 'text/html; charset=UTF-8'
    res.body = body
  end
  trap("INT"){ srv.shutdown }
  srv.start
end
### main #########
setup_file(KICK)
httpd(KICK)

これで目出度くサーブレットになった。webrickもさぞかし満足してる亊でしょう。cgiの名前も消えて、20年のブランクを解消したぞ。もうこれで平成の原人と呼ばれなくなるな。

bugを探して1万行

Debianでは辞書を引いてくれない問題の検証。別の角度からって亊でcurlを使ってみた。まずは優等生なOpenBSDで確認。

vbox$ curl -sSv -X POST 'http://localhost:8080/ejdict' --data 'ew=ruby'
 *   Trying 127.0.0.1:8080...
 * Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /ejdict HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.0
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
>
 * Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< Server: WEBrick/1.6.0 (Ruby/2.7.2/2020-10-01)
< Date: Wed, 13 Apr 2022 04:01:35 GMT
< Content-Length: 266
< Connection: Keep-Alive
<
<html><head><title>ejdict</title></head><body>
<h2>SQL search ruby% limit 5</h2>
<dt>ruby</dt>
<dd>〈C〉紅玉,ルビー / 〈U〉ルビー色,深紅色(しんこうしょく) / ルビー色の,深紅の,
  本発のプログラミング言語</dd>
</body></html>
 * Connection #0 to host localhost left intact

こちらは、Debian(64bit)の結果。綺麗さっぱりと辞書を引くのを拒否って塩梅。

sakae@pen:~$ curl -sSv -X POST 'http://localhost:8080/ejdict' --data 'ew=ruby'
  :
<html><head><title>ejdict</title></head><body>
<h2>SQL search ruby% limit 5</h2>
</body></html>
 * Connection #0 to host localhost left intact

tmux de logging

Ruby初心者のためのByebugチュートリアルを見るとトレースが出来るみたい。

ログに落そうと byebug -t gd.rb |& tee LOG とかやると、固まってしまった。何故って、追求はしないで、ならば、tmuxでログって方向に舵をきる。ああ、突然出て来たgd.rbは、app.rbを改名したもの。期待を込めて、グッドデザイン賞ね。自分で言ってれば世話がないな。兎も角、FreeBSDでも普通に動いたぞ。

.tmux.conf

# start logging
bind-key C-p pipe-pane -o '/bin/sh -c "while read -r LINE; do echo \"\${LINE}\" >> /tmp/TMUX.log; done "' \; display-message "Logging start."

# stop logging
bind-key C-q pipe-pane \; display-message "Logging end."

prefix C-p, prefix C-q で、ロギングをコントロール出来るはず。それからログの場所も/tmpを指定するも、全く無視されて$HOMEに出来ちゃった。出来上がったのは10000行を越てた。

別なロギングの方法として、tmuxが内部に蓄えているデータをファイルに落とす亊が出来る。

tmux capture-pane -p -S -1000 > TMUX.log

-pで標準出力へ。-S で、今迄ログされてる行を遡って行数分を出力(遡るんで、行数はマイナスで指定する)。ちょい使いには、こちらの方が便利そう。aliasで、tlogとか設定しておくとよい。

[sakae@fb ~]$ head ~/20220414-073227-4-1.0.log
[2022_0414_073240_N] byebug -t gd.rb
Tracing: /tmp/ruby-cgi/gd.rb:4 require 'webrick'
[2022_0414_073240_N]
[2022_0414_073240_N] [1, 10] in /tmp/ruby-cgi/gd.rb
[2022_0414_073240_N] 1: # Usage: run this script and then firefox http://localhost:8080/
[2022_0414_073240_N] 2: # warning index.htm file auto maked in current dir
[2022_0414_073240_N] 3:
[2022_0414_073240_N] =>  4: require 'webrick'
[2022_0414_073240_N] 5: require 'sqlite3'

後は、出来の惡いDebianと対比させればいいんだな。難儀なこったい。

道しるべ

千里も道も一歩から、って昔の人は良い亊を言ってるな。でも、現代は忙しいのよ。出て来たログをほいほいとスキップしましょ。

 /tmp/ruby-cgi/gd.rb:40     stmt.execute().each do |rb|
 /usr/local/lib/ruby/gems/2.7/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:62       reset! if active? || done?
 /usr/local/lib/ruby/gems/2.7/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:95       !done?
 /usr/local/lib/ruby/gems/2.7/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:64       bind_params(*bind_vars) unless bind_vars.e\
mpty?

こんな風に、gd.rbを頼りに進めば大丈夫。潜っていけよ。

差分を取ってみたぞ。

冒頭に出て来た検索プログラムをref.rbとしてトレース。辞書を引かない奴(一体形)をgd.rbとした。分かり易い所で、データベースをオープンする所から掲載する。

diff -y REF.log GD.log な結果。醜かったらスマソ。

ref.rb:5   db = SQLite3::Database.new("/usr/share/dict/ejdict | gd.rb:34     db = SQLite3::Database.new("/usr/share/dict/ejdi
database.rb:66       mode = Constants::Open::READWRITE | Cons   database.rb:66       mode = Constants::Open::READWRITE | Cons
database.rb:68       if file.encoding == ::Encoding::UTF_16LE   database.rb:68       if file.encoding == ::Encoding::UTF_16LE
database.rb:75         mode = Constants::Open::READONLY if op   database.rb:75         mode = Constants::Open::READONLY if op
database.rb:77         if options[:readwrite]                   database.rb:77         if options[:readwrite]
database.rb:82         if options[:flags]                       database.rb:82         if options[:flags]
database.rb:89         open_v2 file.encode("utf-8"), mode, zv   database.rb:89         open_v2 file.encode("utf-8"), mode, zv
database.rb:92       @tracefunc        = nil                    database.rb:92       @tracefunc        = nil
database.rb:93       @authorizer       = nil                    database.rb:93       @authorizer       = nil
database.rb:94       @encoding         = nil                    database.rb:94       @encoding         = nil
database.rb:95       @busy_handler     = nil                    database.rb:95       @busy_handler     = nil
database.rb:96       @collations       = {}                     database.rb:96       @collations       = {}
database.rb:97       @functions        = {}                     database.rb:97       @functions        = {}
database.rb:98       @results_as_hash  = options[:results_as_   database.rb:98       @results_as_hash  = options[:results_as_
database.rb:99       @type_translation = options[:type_transl   database.rb:99       @type_translation = options[:type_transl
database.rb:100       @type_translator  = make_type_translato   database.rb:100       @type_translator  = make_type_translato
database.rb:725       if should_translate                       database.rb:725       if should_translate
database.rb:732         NULL_TRANSLATOR                         database.rb:732         NULL_TRANSLATOR
database.rb:101       @readonly         = mode & Constants::O   database.rb:101       @readonly         = mode & Constants::O
database.rb:103       if block_given?                           database.rb:103       if block_given?
ref.rb:6   stmt = db.prepare("SELECT word,mean FROM items WHE | gd.rb:35     stmt = db.prepare("SELECT word,mean FROM items W
database.rb:147       stmt = SQLite3::Statement.new( self, sq   database.rb:147       stmt = SQLite3::Statement.new( self, sq
database.rb:148       return stmt unless block_given?           database.rb:148       return stmt unless block_given?
ref.rb:7   stmt.bind_param(1, ew)                             | gd.rb:36     stmt.bind_param(1, ew)
ref.rb:8   stmt.execute().each do |res|                       | gd.rb:38     body = "<html><head><title>ejdict</title></head>
                                                              > gd.rb:39     body << "<h2>SQL search #{ew} limit 5</h2>\n"
                                                              > gd.rb:40     stmt.execute().each do |rb|
statement.rb:62       reset! if active? || done?                statement.rb:62       reset! if active? || done?
                                                              > utils.rb:192             @queue.clear
                                                              > utils.rb:166             now = Process.clock_gettime(Process:
                                                              > utils.rb:167             wakeup = nil
                                                              > utils.rb:168             to_interrupt.clear
                                                              > utils.rb:169             TimeoutMutex.synchronize{
                                                              > utils.rb:170               @timeout_info.each {|thread, ary|
                                                              > utils.rb:182             to_interrupt.each {|arg| interrupt(*
                                                              > utils.rb:183             if !wakeup
                                                              > utils.rb:184               @queue.pop
statement.rb:95       !done?                                    statement.rb:95       !done?
statement.rb:64       bind_params(*bind_vars) unless bind_var   statement.rb:64       bind_params(*bind_vars) unless bind_var
statement.rb:65       @results = ResultSet.new(@connection, s   statement.rb:65       @results = ResultSet.new(@connection, s
resultset.rb:73       @db   = db                                resultset.rb:73       @db   = db
resultset.rb:74       @stmt = stmt                              resultset.rb:74       @stmt = stmt
statement.rb:67       step if 0 == column_count                 statement.rb:67       step if 0 == column_count
statement.rb:69       yield @results if block_given?            statement.rb:69       yield @results if block_given?
statement.rb:70       @results                                  statement.rb:70       @results
resultset.rb:133       while node = self.next                   resultset.rb:133       while node = self.next
resultset.rb:104       if @db.results_as_hash                   resultset.rb:104       if @db.results_as_hash
resultset.rb:108       row = @stmt.step                         resultset.rb:108       row = @stmt.step
resultset.rb:109       return nil if @stmt.done?                resultset.rb:109       return nil if @stmt.done?
resultset.rb:111       row = @db.translate_from_db @stmt.type | gd.rb:44     stmt.close(); db.close()
statement.rb:118       must_be_open!                          | gd.rb:45     body << "</body></html>\n"
statement.rb:126       if closed?                             <
statement.rb:119       get_metadata unless @types             <
statement.rb:136       @columns = Array.new(column_count) do  <
statement.rb:137         column_name column                   <
statement.rb:137         column_name column                   <
statement.rb:139       @types = Array.new(column_count) do |c <
statement.rb:140         column_decltype column               <
statement.rb:140         column_decltype column               <
statement.rb:120       @types                                 <
database.rb:717       @type_translator.call types, row        <
database.rb:722     NULL_TRANSLATOR = lambda { |_, row| row } <
resultset.rb:113       if row.respond_to?(:fields)            <
resultset.rb:122         row = ArrayWithTypesAndFields.new(ro <
resultset.rb:125       row.fields = @stmt.columns             <
statement.rb:102       get_metadata unless @columns           <
statement.rb:103       return @columns                        <
resultset.rb:126       row.types = @stmt.types                <
statement.rb:118       must_be_open!                          <
statement.rb:126       if closed?                             <
statement.rb:119       get_metadata unless @types             <
statement.rb:120       @types                                 <
resultset.rb:127       row                                    <
resultset.rb:134         yield node                           <
ref.rb:9     puts("<dt>#{res[0]}</dt>\n")                     <
<dt>ruby</dt>                                                 <
ref.rb:10     puts("<dd>#{res[1]}</dd>\n")                    <
<dd>〈C〉紅玉,ルビー / 〈U〉ルビー色,深紅色(しんこうしょく) / <
resultset.rb:104       if @db.results_as_hash                 <
resultset.rb:108       row = @stmt.step                       <
resultset.rb:109       return nil if @stmt.done?              <
ref.rb:12   stmt.close(); db.close()                          <

ランデブーが失敗した、まれなケースを踏んじゃった? Debian 32,64bitの両方で辞書引の拒否。BSD系は問題ないって亊から、十分に考えられるな。それとも、こういう使いかたは想定してませんって亊? これ以上は、オイラーの手に余るな。

たのしいOSSコードリーディング: Let’s read WEBrick こういう楽しいのがあるんで、トレース結果と突き合せてみるかな。何かヒントが見付かるかも知れないぞ。

諦めきれないオイラーはOpenBSDで採取して比べてみた。左がOpenBSDで右がDebian。 OpenBSDには、diff -y がなくて、代わりにsdiffが用意されてた。-w は画面サイズ指定。

vbox$ sdiff -w 126 OB.log GD.log

resultset.rb:108       row = @stmt.step                         resultset.rb:108       row = @stmt.step
resultset.rb:109       return nil if @stmt.done?                resultset.rb:109       return nil if @stmt.done?
resultset.rb:111       row = @db.translate_from_db @stmt.type | gd.rb:44     stmt.close(); db.close()
statement.rb:118       must_be_open!                          | gd.rb:45     body << "</body></html>\n"
statement.rb:126       if closed?                             <
statement.rb:119       get_metadata unless @types             <
statement.rb:136       @columns = Array.new(column_count) do  <
statement.rb:137         column_name column                   <
statement.rb:137         column_name column                   <
statement.rb:139       @types = Array.new(column_count) do |c <
   :

statement.rb:139以降を省略しちゃったけど、この後の流れはREF.logと同一だった。

rust-cgi

面白い物を見付た。cgiをサポートしないWEBサーバーにcgiを追加するものらしい。 早速行ってみよー。 rust-cgi

// How many bytes do we have to read for request body
// A general stdin().read_to_end() can block if the webserver doesn't close  things
let content_length: usize = env_vars.get("CONTENT_LENGTH")
    .and_then(|cl| cl.parse::<usize>().ok()).unwrap_or(0);

let mut stdin_contents = vec![0; content_length];
stdin().read_exact(&mut stdin_contents).unwrap();

前回苦労した所が、こんなにもスマートに記述出来るんだ。まだまだ、オイラーはひよこだな。

python + ruby

ああ、それからrust-cgiに出て来る説明に疑問が有っtので、pythonのコードを覗いてみたよ。そしたら、–cgiってオプションを付けるとCGI対応になる亊が判明。

PythonでHTTPDして、rubyのcgiを動かすっていうコラボをやってみた。

sakae@pen:/tmp/t$ tree
.
├── htbin
│   └── sql.rb
└── index.html

htbinの代わりにcgi-binでもOK。サーバーをキックするindex.htmlを調整してね。

sakae@pen:/tmp/t$ python3 -m http.server --cgi 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
127.0.0.1 - - [16/Apr/2022 15:03:30] "GET / HTTP/1.0" 200 -
127.0.0.1 - - [16/Apr/2022 15:03:38] "POST /htbin/sql.rb HTTP/1.0" 200 -
127.0.0.1 - - [16/Apr/2022 15:03:58] "POST /htbin/sql.rb HTTP/1.0" 200 -

ちょいとした疑問が福を呼ぶな。それもこれもrustをやっていればこそのものです。

たまにrubyをやると、緩くていいなと思うぞ。型について余り気にする必要無いしね。 rubyで関数を呼ぶ時、思わず引数の型を書こうとして、指が止ってしまったのは、秘密事項だ。


This year's Index

Home