Chienomi

Linuxにおけるプログラミング言語の有力選択肢

Live With Linux::tips

YouTubeのほうで頂いたコメントが、結構掘り下げられる内容だったので、記事にすることにした。

内容は以下の通りである。

linuxソフトを作成するための第一選択って何の言語なんでしょうか windowsでいうc#のようなものはありますか?

この答えは「基本的にないが、選択肢は山ほどある」なのだが、実は結構深い。 すでにプログラミングスキルがあり、その上でWindowsからLinuxへの移住を考える人もいるだろうから、ちゃんと掘り下げようと思う。

Windowsにおける.NETの話

Windowsにおける.NET (C#) というのは非常に特殊な立ち位置である。

そもそも、.NET登場前は、Windowsネイティブアプリ(WIN32 APIを使うアプリ)の言語は基本的にCだった。C++も利用可能であったが、いくらかの制限があり、Cのほうに少しアドバンテージがあった。

だが、それはかなりハードルの高い話であった。また、WIN32 APIを使うネイティブアプリケーションのコンパイルにかかるコストが高かった(Visual C++に無料版が出たのは結構あとのことだと理解している)。

このことから「簡単に」作れる言語としてVisual Basicがあった。 特にVisual Basic 4.0はWindows 95において初心者向けの有力な選択肢であった。

だが、Visual Basicにはかなり問題があった。 特にレシーバ検索が異様に遅い上にメモリを浪費するという問題は深刻であった。

のちにVB .NETが登場するが、こちらはそこまで利用された印象はない。

結局、Microsoft C/C++, Visual C++を使ってC/C++によってWIN32アプリを作るのは困難を伴い、Visual Basicによるプログラミングは不十分である、という状況があった。 そもそも当時Microsoftは「ホビイストによるプログラミング」というもの自体を忌避していた面があるため、一般の人がプログラミングする世界に対して否定的であり、Windowsプログラミングは一般に対して閉じていた。「プロであれば、Visual C++を使うはずだ」という姿勢であり、「ホビイストならVisual Basicで十分だ」である。

結果的に、その中間的な(比較的扱いやすい)Windowsプログラミングの手段としてはDelphiが結構使われていた。 DelphiはSmalltalkのように単純な言語ではなく、その生態系までも含むものだが、言語自体はPascalの系統である。

C#/.NETの登場はそのような状況を大きく変えるものであった。 そもそもC#はDelphiの後継ともいうべきもので、Delphi開発陣を多く取り込んで作られたものだ。 つまり、そのポジション的にはDelphiに近い。言語的にはDelphiをJavaに寄せたようなもの、とも理解できる。

Delphiがそうであったように、開発環境を一般個人に対しても開いており、言語も扱いやすいものである。 必然的に、当たり前のようにC/C++でプログラミングをしていた(Microsoft製品を購入していた)人たちを除けば、WindowsプログラミングはC#/.NETに吸われることになる。 だから、ホビイスト、ビギナー、あるいは開発効率を重視する企業まで、広くWindowsプログラミングにC#が使われるようになったわけだ。

この流れは前提として、 WindowsはWindowsのプログラミングにおいてWIN32APIの利用を強いる ということがある。 これは、Microsoftの言語処理系を必須とするという意味ではないが、WIN32APIを使う以上Windows専用のプログラムを書くことを強いられる。システムコール的にもUnix系OSとは互換性がないため、結果的にWindows上でのプログラミングというのはMicrosoftがコントロールすることになるし、Windowsでは「C#を使わざるをえない」側面があるのだ。

実際、多くの言語はWindows上では完全に動作しない。 Perlは非常に複雑なトリックによってWindowsで動作するが、それでも完全ではない。 RubyはそもそもWindows上では動作せず、MSYS2やWSLなど、Unixエミュレーションによって動作させることができる。 PHPはPHP8からWindowsをサポートしないが、そうなった経緯はPHP7まではMicrosoftの積極的な関与によってWindowsがサポートされてきたからである。

Windows上できちんと動作する、Microsoftが関与しない言語はかなり少なく、ターゲットプラットフォームがそもそもUnixではないPythonや、クロスプラットフォームを強く意識するJavaなどである。

Linuxにおける.NET的存在

だが、Linuxにおいてはそのような支配は存在しない。 それはLinuxにおいてというよりも、Unixにおいてといったほうが近いが、MacはAppleが支配しており、Cocoaフレームワークが似たような位置にある。

だが、例えばLinuxやFreeBSDにおいては、そのような支配をする立場にある者がそもそもいない。 商用UNIX(例えばSolarisやAIX)はそのような者がいるわけだが、そこにリソースを割いていないので、やはりない。その意味では、Mac OS Xは普通はなかなか割けないリソースを大量に投入しているという意味ですごい。

LinuxはLinuxそのものを使う上ではLinux自体がCライブラリであるため、C言語で書く必要性が少しある。 ただし、C言語のライブラリを動作させることができれば、C言語でなくても構わない。カーネル空間で動作させる場合はもう少し制約が加わるが、ユーザープログラムであればC言語のライブラリを動かすことができる、が十分条件になる。

十分条件であるのは、そもそもCライブラリを使わなくてもシステムコールを用いてLinuxを動作させることができるからだ。 システムコールを呼び出すためのライブラリはC言語で書かれており、そのためにC言語のライブラリを利用する必要があるのだが、ライブラリの利用はシステムコールを呼び出す唯一のインターフェイスというわけではないため(バイナリレベルでも呼べるし、procファイルシステムもある)、C言語の利用を強いているわけではない。 さらに、これは「言語処理系がC言語のライブラリをドライブしてシステムコールを呼べば良い」ということであるから、ユーザーレベルではC言語の利用には関わらないと言ってよい。

Linuxでのプログラミング言語に適不適があるとすれば、それは 文化とコミュニティによるものである としか言えない。

UnixにおけるC++

Unixのコアなライブラリ、あるいは高度のライブラリの多くはC++で書かれている。 Cで書かれているものもあるが、おそらくC++で書かれているものに当たることが多いだろう。

C++のライブラリは当然ながらC++で利用できる。 さらに、CのライブラリもC++から(コードは汚くなるかもしれないが)利用可能である。 LinuxプログラミングにおいてC++が得意であればライブラリの利用のハードルがとても下がる、というのはまず間違いなく言える。

ただし、C++が唯一のバインディングである人気のライブラリ、というのはあまりない。 ほとんどの場合、他の言語にラッパーがあるか、バインディングがあるか、移植されている。

これは言語選択に明確な影響を与える。

Rubyは極めて強力な言語であり、Rubyを凌ぐ機能を内包する言語処理系は存在しないだろう。 だが、Rubyのコミュニティはやや弱く、熱意に欠ける。どういうことかというと、ライブラリがメンテナンスされていないことが多い。結果的に利用可能なライブラリに不足が生じ、書けないプログラム、というのが発生する。

否、別に書けないことはない。全部自分で書けば良い。 要は、「ライブラリが存在していなくてもどうにかなるか」が言語選択に影響するのだ。

Rubyのライブラリが不足しているといっても、総合的に言えばRubyは恵まれているほうだ。 Lua, Common Lisp, Erlangなどのユーザーはもっと苦労するし、Effeil, D, Nim, Zigなどマイナーな言語のユーザーであれば基本自分で書くくらいの気概が必要になるだろう。 (といっても、C言語へのインターフェイスがあれば最悪手段はあるみたいなことになるし、NimにはPythonのインターフェイスがあるのでそんなこともないかもしれない)

デスクトップアプリケーション(ウィンドウアプリケーション)を書きたいのであれば、GTKはCライブラリで、QtはC++ライブラリである、ということを頭に入れておくと良いだろう。 その他で言うと、EFLはC、FLTKはC++であり、便利なラッパーライブラリであるWxWidgetはC++である。

UnixにおけるPython

Pythonは世界的に人気のある言語だが、Pythonにおいて重要なポイントは「高生産性言語として確固たる地位を持っている」ということである。

「高実行効率言語」という話をすると、C, C++, Java, C#, Rust, Goあたり(GoogleはJavaからGoへと切り替えた面もあるようだ)になるわけだが、企業としてそれらの言語のみで開発することは機動性を封じられる面もある。 そこで高生産性言語を併用するということが行われるわけだが、この場合だいたいPythonが採用される流れがある(それがサーバーであれば、最近はJavaScriptが採用されることも多いが)。

だから多くの人が「生産性を求めるならPythonにしよう」みたいな考えを持つことになり、結果的にライブラリのPythonバインディングは増える。

機械学習でPythonが使われるのもそういう理由で、機械学習で使われるライブラリは基本的にPythonネイティブではないし、Python自体が機械学習に向いているわけでもない。 だが、PythonバインディングがあればPythonで書けるし、そのハードルも下がる。

この結果Pythonが「他の言語のバインディングを用意する場合の第一候補」になり、そうなるとライブラリ側で公式にPythonバインディングを用意したりすることが少なくない。なぜならば、Pythonバインディングを用意することでそのライブラリは一層多くの人に使ってもらえる。 そして、そのようなバインディングは、有志によって作られるバインディングと違い、ライブラリのバージョンアップに取り残されるリスクが少ない。

もちろん、Pythonはそれらのライブラリが使用している言語と比べ低速である可能性が高い。 だが、処理の中核がライブラリの中にあるのであればそれはあまり問題にはならない。 なぜならば、Pythonはそのライブラリを呼び出すためのただのインターフェイスであり、ライブラリの処理はライブラリのバイナリで行われるからだ。 機械学習は実行効率が重要なものだが、その実行は圧倒的にライブラリ内で行われる部分が大きいから、Pythonを利用するデメリットがあまり目立たない。

結果的に、PythonはC++に次ぐほどに、あるいはC++以上にLinuxプログラミングでは確実性のある言語である。

UnixでのJava

UnixにおいてJavaはそれらの羈絆から逃れる手段としても用いられる。

これはメリットでもデメリットでもある。Javaの場合、基本的にJavaのライブラリを使うしかない。 これによって、プラットフォーム側のライブラリ事情から解放されるが、逆にJavaのライブラリ事情に束縛される。

Javaを使うことでクロスプラットフォームを実現できることはもちろん、他のプラットフォームにおけるプログラミング知識をほぼほぼそのまま持ち込めるという点も大きい。 例えばC++でWIN32APIを利用していた人であっても、Linux上でC++プログラミングをするためにはWIN32APIを使わないプログラミングを習得する必要があり、ウィンドウアプリケーションであればQtを習得する必要もあるだろう。 しかし、Javaの場合はほとんど今までどおりのコードで動くのだ(JavaでWIN32APIを使うようなことをしていなければ)。

C#もJavaの位置づけを狙っていたし、実際Linux上で動作する.NET CoreやMonoも存在するが、現実的にはJavaのような利点はない。 なぜならば、ほとんどのC#ユーザーは(競技プログラミングなどで利用しているケースを除き)WIN32APIによるプログラミングをしているのであり、動作としても知識としても移植性がない。

また、Pythonなどクロスプラットフォームで動作する言語を使う場合でも、やはりそのまま持ち込むのは難しい。PythonでWindows向けのデスクトップアプリケーションを作る場合、やはりQtを使うよりはWIN32APIを使うことのほうが有力な選択肢になるからだ。

こうした問題からだいたい解放される、というのはJavaの非常に大きな特色であり、クロスプラットフォームでの動作を狙うかどうかに関わらず、Unixプラットフォームにおける悩ましい要素を避けるためにJavaを選ぶという人は決して少なくない。

UnixにおけるJavaScript

JavaScriptもJavaに近い効果を発揮する。 GUIアプリケーションにおいてもだ。

多くの場合、それはElectronである。 ElectronはGitHubがAtomのために作ったものだが、Visual Studio Code, Discord, Slack, Skypeなどでも利用されている。

この話におけるJavaScriptのクロスプラットフォーム性・独立性は純粋にChromiumのクロスプラットフォーム性から来ている。Node.js自体がheadless Chromiumとでも言うべきものであり、Chromiumがクロスプラットフォームで動作するならばNodeもクロスプラットフォームである、が通用する。

NodeはElectronによってウィンドウ部分だけがシステムから切り離されるわけではない。 Java同様、NodeのライブラリはシステムライブラリではなくNodeの閉じた生態系のライブラリであることが普通であり、システムからは切り離され、Nodeに束縛される。 Java同様、Nodeはライブラリが充実しているため、Nodeで生きていくことは十分に現実的だ。

Nodeはコードに少し特殊性があり、プログラムの性質により適不適が他の言語よりもはっきりと出る。 これを克服するため、他の言語にブリッジする方法があるが、これはNode以外からブリッジするのとはまた違う。

UnixにおけるPerl

UnixにおいてPerlは少し特殊な位置づけだ。

様々な事情を踏まえた上で文字列操作が得意な言語(文系言語と言ったりする)は結構レアで、特にUnix文化である「行指向テキスト」への適性が高い言語というとだいぶ限られる。

Rubyは非常に特殊で、おそらく文字列操作に最も強い言語だ。 特に日本人にとっては「複雑な事情を持つテキストデータ」は日常茶飯事なのでRubyが良い。

だが、Perlは行指向テキスト処理に特化した機能を持っており、そのようなケースに適している。

これは、単純に適しているというだけの話ではなく、「コマンドとして実行できる」という意味だ。 つまり、PerlはSedやAwkのすごい版のような位置づけで、Perl単体で使う場合はもちろん、フィルタとしても非常に使いやすいものになっている。

だから、findやgrepやsedと同じレベルで、「覚えておくべきコマンド」としてPerlがある。

もちろん、そのコマンドの威力はPerlの知識に比例する。 だが、それを理由に全てをPerlで書くPerlスペシャリストになろう、という考えは多分あなたを幸せにしない。 なぜならば、他のより適性の高い言語を習得するコストよりも、Perlであらゆるプログラムを書くコストのほうが高いからだ。

Rubyでの生活

Rubyは極めて強力な言語であるため、自分で書くという意味では比類なき強力さを発揮する。 また、Ruby自体に組み込まれているライブラリも豊富で、素の状態でできることも非常に幅広い。

だが、Rubyのサードパーティライブラリはかなりコアなライブラリであってもメンテナンスされていないものが少なくなく、サードパーティライブラリを使う場合、プログラムのライフタイムには少し不安がある。

GUIプログラミングは、Ruby GNOMEがGTKのバインディングとして利用可能だ。 Ruby GNOMEはその名前に反してGNOMEではなくGTK向けのライブラリであるし、GNOME2といいつつGTK3にも対応している。 Ruby GNOMEはWindows, Macにも対応しており、かなり安定している。 そのため、GTKを使うことを前提としている人にとっては、Rubyは悪くない選択肢に見える。

一方、RubyのQtバインディングは非常に不安定で、メンテナンスされておらず、クロスプラットフォームでもない。

これは、Rubyでウェブブラウザを提供したい場合に大きく響いてくる。 GtkはWebKitを使うが、GtkWebkitは非常に取り残されたバージョンであり、十分なレンダリング能力を持たない。そのため、ブラウザの実装はQtWebengineを使うほうが有力な選択肢なのだが、Rubyではそれは難しい。

現実的に、ほとんどの場合Rubyは最も快適で、優れた選択肢であり続けるが、限定的な状況では悩ましいこともある。これは、「C++やPythonと比べると活躍の場は限られる」程度の話だが。

UnixのGUIプログラミング

もっとも、単純に「自分のプログラムにウィンドウが欲しい」程度の話であれば、そこまでのことを気にする必要はない。

私のプログラムには

  • PythonでQtを使い(PyQt)、ロジックもPythonであるもの
  • PythonでQtを使うが、ロジックはRubyであるもの
  • RubyでGTKを使うもの(Ruby GNOME)

というのもあるが、むしろ多くはZenity、あるいはYadを使っている。

以下は現行の@Nowのコードである。

#!/usr/bin/ruby

PPID=$$
DOC_DIR = `xdg-user-dir DOCUMENTS`.chomp

Dir.mkdir "#{DOC_DIR}/atnow" unless File.exist? "#{DOC_DIR}/atnow"
File.open("#{DOC_DIR}/atnow/now", "w") {|f| nil } unless File.exist? "#{DOC_DIR}/atnow/now"

wp = IO.popen(["yad", "--notification", "--listen", '--image=user-available', '--text="@Now"', '--item-separator=`', "--command=kill -HUP #{PPID}"], "w")

update_menu = ->() {
  lines = File.readlines "#{DOC_DIR}/atnow/now"
  if lines.length > 520
    File.open("#{DOC_DIR}/atnow/archive-#{Time.now.strftime("%Y%m%d-%H")}", "w") do |f|
      f.puts lines.first(500)
    end
    lines = lines[500..]
    File.open("#{DOC_DIR}/atnow/now" ,"w") do |f|
      begin
        f.flock(File::LOCK_EX)
        f.puts lines
      ensure
        f.flock(File::LOCK_UN)
      end
    end
  end
  wp.puts("menu:" + lines.last(20).map {|i| fs = i.strip.sub(/^.*?\t/, "")
  fs.length > 64 ? sprintf('%s…`zenity --info --text="%s"`user-available', fs[0, 63], fs) : sprintf('%s``user-available', fs) }.join("|"))
}
update_menu.()

Signal.trap() do
  text=`yad --entry --title="Your Tweet" --text="What's Happen?" --image=gtk-dialog-question --width=450`
  if text.length > 0
    File.open("#{DOC_DIR}/atnow/now" ,"a") do |f|
      begin
        f.flock(File::LOCK_EX)
        f.puts([Time.now.strftime("%Y-%m-%d\t%H:%M:%S"), text.tr('|`', "/'")].join("\t"))
      ensure
        f.flock(File::LOCK_UN)
      end
    end
    update_menu.()
  end
end

begin
  File.open("#{ENV["HOME"]}/.atnow.pid", "w") {|f| f.print PPID}
  Process.waitpid(wp.pid)
ensure
  File.delete("#{ENV["HOME"]}/.atnow.pid")
end

Rubyで書かれているが、GUI要素はZenityとYadを使っている。 この場合、Yadはシステムトレイでの表示と動作を担っており、ポップアップウィンドウはZenityにしている。手間を考えてZenityを使った(Yadが入っていてZenityが入っていない環境は少ないだろうし、そもそも自分が使うのだから入れれば良い)が、Yadだけにすることもできる。

My Browser Profile ChooserはZenityとnotify-send(1)を利用している。 現行のMy Browser Profile Chooserは@Now同様にRubyで書かれているが、以前はZshで書かれており、表示部分に関しては同じ仕組みだった。

あくまでデスクトップ通知したいだけであれば、libnotifyを使えばさらに簡単である。 notify-send(1)によって一発で通知が表示できる。言語から直接利用するにしてもせいぜい10行程度で書けるし、非常に多くの言語で使える。

Node.jsで他のプログラムにブリッジ

他の言語にブリッジする場合、重要なのはコマンド実行で渡せる場合を除けば、同期的にメッセージを送信し、受信することが可能であることである。

例えば次のコードを見てみよう。

get_profile($user)

これは関数呼び出しであり、同期的に実行される。そして、その結果を値として受け取る、ごく普通のコードだ。

他のプログラムにブリッジするためには、これが単純に他のプログラムとやりとりするものになっていれば良い。 シェルスクリプトは基本的に他のプログラムに対してブリッジする構造であるから、シェルスクリプトで書けるようになっていれば良い。

declare profile="$(echo $user | get_profile)"

コマンドとして実行されるのであれば、ブリッジされるプログラムはフィルタとして動作する、つまり標準入力から値を受け取り、標準出力に結果を返すインターフェイスを持てば良い。 ウィンドウのコールバックのように、頻繁に呼び出されるものであれば、UNIXドメインソケットを用いるのが常套手段だ。

while sock = server.accept
  req = JSON.load(sock)
  result = remote_methods[req["method"]].(req["args"])
  JSON.dump(result, sock)
  sock.close
end

ソケットさえ持っていれば良いので、非同期で処理できるようにもできる。

def remote_exec(sock)
  req = JSON.load(sock)
  result = remote_methods[req["method"]].(req["args"])
  JSON.dump(result, sock)
  sock.close
end

while sock = server.accept
  Thread.new { remote_exec(sock) }
end

しかし、Nodeはどちらのやり方でもスマートに書くのが難しい。 そこで、Nodeを使う場合、HTTPサーバーとして書くのが良い。

例えばこんな感じだ。

require 'sinatra/base'
require 'json'

class RubyBridge < Sinatra::Base
  BRIDGE_PATH = "/tmp/ruby-ipc-bridge.sock"
  Profiles = {
    "John" => "Fooo"
  }

  set , "thin"
  set , BRIDGE_PATH
  post '/' do
    begin
      arg = JSON.load(request.body)
      JSON.dump({"value" => Profiles[arg["name"]]})
    rescue => e
      JSON.dump({
        "err" => {
          "class" => e.class,
          "message" => e.to_s
        }
      })
    end
  end
end

RubyBridge.run!

ここではSinatra/Thinを使っている。SinatraはRubyらしい、実に良いフレームワークだ。(Railsとは大違いだ!!) Sinatraの名に恥じない。 Thinを使うのは、UNIX Domain Socketを使いたいためである。

これに対してNodeからはHTTPで投げかける。ここではAxiosライブラリを使用する。

const get_profile = (arg) => {
  axios({
    socketPath: bridge_path,
    url: "/",
    method: "post",
    data: arg
  }).then(res => return JSON.parse(res.data))
}

Rubyなら簡単にwebサーバーが用意できるが、そんな言語は多くない。 力づくでHTTPをRequest Bodyだけに剥いても良いが、意外とめんどくさい。 私ならRubyで中継する方法を取る。

require 'sinatra/base'
require 'json'

class RubyBridge < Sinatra::Base
  BRIDGE_PATH = "/tmp/ruby-ipc-bridge.sock"
  NEXT_PATH = "/tmp/lua-ipc-bridge.sock"

  set , "thin"
  set , BRIDGE_PATH

  post '/' do
    begin
      bridge = UnixSocket.new(NEXT_PATH)
      JSON.dump({
        "request" => request.path_info,
        "arg" => JSON.load(request.body)
      }, bridge)
      bridge.close_write
      result = bridge.read
      bridge.close
      result
    rescue => e
      JSON.dump({
        "err" => {
          "class" => e.class,
          "message" => e.to_s
        }
      })
    end
  end
end

RubyBridge.run!

これでNode以外と同様、SocketでのIPCが可能になった。

結び

以上を踏まえて、「特に考慮せずとも良い」という意味 ではなく、 Linux上でプログラミングするならば好きな言語を選べばよいとなるし、使えると便利だと思う言語をいくつか覚えておいたほうが良い。

どうしても「最も汎用性のある言語」を求めるならPythonをお勧めする。 たいていのライブラリがPythonで動くし、Pythonが使えればだいたいの作りたいものを作ることができる。だが、それでもシェルスクリプトくらいはそれとは別に使えたほうが良いし、そのためにgrep, find, sed, awk, そしてperlなどもある程度覚えておくほうが良い。

結局、「選択肢が多いからこそ場合によって最適解は異なる」であり、個人的には特定の言語を唯一の存在のように扱うこと自体があまりLinuxらしくないと感じる。

選択基準は人それぞれだが、冒頭の内容に対して一言で答えるならば「ない」が答えになる。

だが、その裏には様々な文化的・歴史的差異であったり、あるいは唯一の選択はないにしても考える上で重要な事柄はあるため、掘り下げてみた。