Ruby(Windows)で改行付き文字列データを扱う場合はテキストモード(バイナリモード)に気をつけろ!!
はじまり
Rubyに限った話ではないのかもしれないですが、Rubyで改行を含むテキストデータを送受信するようなプログラムをWindowsで書いていた時に思った通りに処理されなくて嵌ってしまったので、自戒を込めてブログに残しておきます。
結論
Windowsで CR/LF をそのまま受け渡したいのであれば、IO#binmodeを設定しよう!!
詳細
例えば以下のようなプログラムがあったとします。
write.rb
$stdout.write "1\r2\n3\r\n" $stdout.flush
read.rb
while char = $stdin.getc p char end
以下のようにコマンドプロンプトから実行してみます。
> ruby write.rb | ruby read.rb
出力は以下のようになります。
"1" "\n" "2" "\n" "3" "\n" "\n"
\r
がすべて \n
に変換されてしまっています。 これは IO#getc
がそのような変換をかけるために発生します。これに対応するには2種類の方法があります。一つは IO#read
を length 1 で呼び出すようにコードを置き換える方法、もう一つは IO#binmode
を設定する方法です。
read.rb (IO#read使用)
while char = $stdin.read(1) p char end
read.rb (IO#binmode使用)
$stdin.binmode while char = $stdin.getc p char end
IO#binmode
は IOオブジェクトをバイナリモードに設定するためのメソッドになります。バイナリモードというのは Windows 特有のモードのようで、通常はテキストモードになっています。このモードを変更すると改行の処理が変化します。テキストモードの場合は puts "\n"
のようなコードを実行すると \r\n
が出力され、getc
に対して \r\n
が入力されると \n
が入力されたものとして処理されます。バイナリモードにすることで、コードに記述した通りの出力となり、入力も \r
と \n
を別々に処理されるようになります。Rubyのリファレンスマニュアルにも記載されています。
Windows の IO にはテキストモードとバイナリモードという2種類のモードが存在します。これらのモードは上で説明した IO のエンコーディングとは独立です。改行の変換にしか影響しません。
さて、上記のコードを実行した出力を見てみましょう。どちらも以下の通り同じ出力になります。
"1" "\r" "2" "\r" "\n" "3" "\r" "\r" "\n"
なにかおかしいですね。1
の後の \r
がそのまま出力されているのはいいのですが、今度は \n
が \r\n
と出力されています。これは先ほど説明したWindowsのIOモードの問題が出力側に残っているのが原因です。出力側に対策を入れていないので、出力側で $stdout.write "\n"
というコードを実行すると、\n
を \r\n
に変換して出力します。この問題に対応するためには 出力側にも IO#binmode
を追加します。
write.rb (IO#binmode使用)
$stdout.binmode $stdout.write "1\r2\n3\r\n" $stdout.flush
出力は以下のようになります。
"1" "\r" "2" "\n" "3" "\r" "\n"
期待した出力になりました。めでたしめでたし。
注意点
今回は標準入出力にbinmodeを設定して対策しましたが、一度binmodeを設定すると、closeするまでバイナリモードのままとなります。標準入出力は再オープンできないので、一度binmodeを設定してしまうとそのプログラム内では二度とテキストモードに戻すことができませんので注意が必要です。
まとめ
- 入出力をパイプでつなぐような処理を記載するとき改行が入るようなデータを扱う場合、かつ Windows が関係する場合は極力バイナリモードでデータの受け渡しは行うこと。
- 出力側は
IO#binmode
を使用することでバイナリモードとなる - 入力側は
IO#getc
の代わりにIO#read(1)
を使用するか、出力側と同様にIO#binmode
を使用することでバイナリモードとなる
Hotwire チュートリアルをする
Hotwire とは
Railsの作者 DHHが提唱する新しいSPAの形を実装したライブラリになります。
今まではサーバーサイドのデータをJSONのようなフォーマットでクライアントに渡し、クライアント側のJavascriptでHTMLをレンダリングすることでSPAを実現するといった流れでしたが、HotwireはサーバーサイドでHTMLをレンダリングし、必要な部分だけをクライアント側で置き換えることで実現されます。
これによって、レンダリングに関わるコードが Ruby で記述できるので、Ruby好きにはうれしいライブラリとなるかと思います。
チュートリアル
なぜかドキュメントの Get Started 的なものは見つからず、 hotwired.dev
のトップページにあるデモ動画が唯一のチュートリアルとなるようです。
ただ、8/13 現在、この通りにやってもうまくいかないのでその部分を交えつつチュートリアルをやっていこうと思います。
Ruby バージョン
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
※ 今回のチュートリアルでは内部で Redis を使いますので、Redisのインストールを行ってからチュートリアルをスタートしてください。
Rails アプリの作成
$ rails new chat --skip-javascript $ cd chat
Gemfile の編集
rails
のバージョンを githubのマスターにします。( 現状はimportmap-rails
がrails
の最新に依存しているみたいだったので )gem 'importmap-rails'
を足しますgem 'hotwire-rails'
を足します
gem 'rails', :github => 'rails/rails' # ← ここは編集 gem 'importmap-rails' # ← ここは追加 gem 'hotwire-rails' # ← ここは追加
各種 gem のインストール
$ bundle update $ bundle install $ rails importmap:install $ rails hotwire:install
ベースのアプリを作成
$ rails g scaffold room name:string $ rails g model message room:references content:text $ rails db:migrate $ bundle install
各種ファイルの編集・作成
config/routes.rb 【編集】
Rails.application.routes.draw do resources :rooms do resources :messages end end
app/models/room.rb 【編集】
class Room < ApplicationRecord has_many :messages end
app/controllers/messages_controller.rb 【作成】
class MessagesController < ApplicationController before_action :set_room, only: %i[ new create ] def new @message = @room.messages.new end def create @message = @room.messages.create!(message_params) redirect_to @room end private def set_room @room = Room.find(params[:room_id]) end def message_params params.require(:message).permit(:content) end end
app/views/messages/new.html.erb 【作成】
<h1>New Message</h1> <%= turbo_frame_tag "new_message", target: "_top" do %> <%= form_with(model: [ @message.room, @message ]) do |form| %> <div class="field"> <%= form.text_field :content %> <%= form.submit "Send" %> </div> <% end %> <% end %> <%= link_to 'Back', @message.room %>
app/views/messages/_message.html.erb 【作成】
<p id="<%= dom_id message %>"> <%= message.created_at.to_s(:short) %>: <%= message.content %> </p>
app/views/rooms/show.html.erb 【編集】
<p id="notice"><%= notice %></p> <%= turbo_frame_tag "room" do %> <p> <strong>Name:</strong> <%= @room.name %> </p> <%= link_to 'Edit', edit_room_path(@room) %> | <%= link_to 'Back', rooms_path, "data-turbo-frame": "_top" %> <% end %> <div id="messages"> <%= render @room.messages %> </div> <%= turbo_frame_tag "new_message", src: new_room_message_path(@room), target: "_top" %>
app/views/rooms/edit.html.erb 【編集】
<h1>Editing Room</h1> <%= turbo_frame_tag "room" do %> <%= render 'form', room: @room %> <% end %> <%= link_to 'Show', @room %> | <%= link_to 'Back', rooms_path %>
app/asserts/stylesheets/application.css
/* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ turbo-frame { display: block; border: 1px solid blue; }
※ ここまでで rails を実行すると、Roomを作成して編集するときに画面遷移なしに編集ボックスが出るようになり、メッセージも遷移なしに追加されます。
turbo_stream フォーマットの追加
app/controllers/messages_controller.rb 【編集】
class MessagesController < ApplicationController # ... 省略 def create @message = @room.messages.create!(message_params) respond_to do |format| format.turbo_stream format.html { redirect_to @room } end end # ... 省略 end
app/views/messages/create.turbo_streaam.erb 【作成】
<%= turbo_stream.append "messages", @message %>
メッセージの相互通信の実現
app/assets/javascripts/controllers/reset_form_controller.js 【作成】
import { Controller } from "@hotwired/stimulus" export default class extends Controller { reset() { this.element.reset() } }
app/views/messages/new.html.erb 【編集】
<h1>New Message</h1> <%= turbo_frame_tag "new_message", target: "_top" do %> <%= form_with(model: [ @message.room, @message ], data: { controller: "reset_form", action: "turbo:submit-end->reset_form#reset"}) do |form| %> <div class="field"> <%= form.text_field :content %> <%= form.submit "Send" %> </div> <% end %> <% end %> <%= link_to 'Back', @message.room %>
app/views/rooms/show.html.erb 【編集】
<p id="notice"><%= notice %></p> <%= turbo_stream_from @room %> <% #省略 %>
app/models/message.rb 【編集】
class Message < ApplicationRecord belongs_to :room broadcasts_to :room end
app/views/messages/create.turbo_streaam.erb 【編集】
<% # Return handled by cable %>
ルーム名のリアルタイム同期の実装
app/models/room.rb 【編集】
class Room < ApplicationRecord has_many :messages broadcasts end
app/views/rooms/show.html.erb 【編集】
<p id="notice"><%= notice %></p> <%= turbo_stream_from @room %> <%= turbo_frame_tag "room" do %> <%= render @room %> <% #省略 %>
app/views/rooms/_room.html.erb 【編集】
<p id="<%= dom_id room %>"> <strong>Name:</strong> <%= room.name %> </p>
参考資料
Microsoft 純正 Dependency Injection ライブラリ試してみた
V-VM を Dependency Injection でつなげる例
using System; using System.Windows; using MessagePipe; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; namespace MessagePipeTest2 { public partial class App : Application { IHost host; private void Application_Startup(object sender, StartupEventArgs e) { host = CreateHostBuilder().Build(); var vm = host.Services.GetRequiredService<MainWindowVM>(); var w = new MainWindow(); w.DataContext = vm; w.Closed += (sender, e) => host.Dispose(); w.Show(); host.RunAsync(); } static IHostBuilder CreateHostBuilder() { return Host.CreateDefaultBuilder() .ConfigureServices((ctx, services) => { services.AddMessagePipe(); services.AddHostedService<Worker>(); services.AddSingleton<MainWindowVM>(); }); } } }
AddSingleton で VM をサービスに登録。必要なタイミングで GetRequiredService でインスタンス化して V の DataContext に設定。
M の部分は AddHostedService でサービスに登録すると同時にインスタンス化している。
VM - M 感は MessagePipe の Pub-Sub で繋げてやり取りする。
↓↓↓↓↓↓↓ VM
using System; using MessagePipe; using Prism.Mvvm; namespace MessagePipeTest2 { class MainWindowVM : BindableBase { private int _value; public int Value { get { return _value; } set { SetProperty(ref _value, value); } } private string _text; public string Text { get { return _text; } set { SetProperty(ref _text, value); } } ISubscriber<MyEvent> subscriber; readonly IDisposable disposable; public MainWindowVM(ISubscriber<MyEvent> subscriber) { this.subscriber = subscriber; var bag = DisposableBag.CreateBuilder(); this.subscriber.Subscribe(Callback).AddTo(bag); disposable = bag.Build(); } public void Callback(MyEvent e) { Value = e.Value; Text = e.Text; } void Close() { disposable.Dispose(); } } }
↓↓↓↓↓↓↓↓ M
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using MessagePipe; namespace MessagePipeTest2 { class Worker : BackgroundService { IPublisher<MyEvent> publisher; MyEvent e; public Worker(IPublisher<MyEvent> publisher) { this.publisher = publisher; e = new MyEvent(); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.Run(() => { while (!stoppingToken.IsCancellationRequested) { e.Value++; e.Text += DateTime.Now.ToString() + "\n"; publisher.Publish(e); } }); } } }
CySharp/MagicOnion を試してみた
CySharp/MagicOnion は以下のリポジトリにあります
基本的にはREADMEを読むか、リポジトリ直下にある sample
を参考にすれば動作はわかると思います。
以下の記事は本家の人のブログ
試した結果はGitHubに置きました。
GrpcService2
はサーバーサイドConsoleApp1
はコンソールベースのクライアントWpfApp1
は WPFベースのクライアントClassLibrary1
はインターフェースを定義しているライブラリ。サーバーとクライアントで共有している
感想
gRPCの使用感で簡単に使えるのはいいかなと思いました。 MessagePack を使用するので通信に乗せるクラスはすべて定義が必要なのがちょっとめんどくさい感じがありますが、いったん定義してしまえばだれでも使えるようになるので大規模開発には重要ですね。一斉同報的な処理とか簡単にできるあたりも好印象で、コネクション毎に処理が分断されている(?)みたいなので無駄に並列処理を意識せずにチャットアプリが簡単に作れるのがいい感じです。
Cysharp/MessagePipe を試してみた。
感想
ブログにも書いてある通り prism の eventAggregator より速いので、今後は prism の代わりにこっちを使ってもいいかなぁと思いました。
App.xaml
<Application x:Class="MessagePipeTest.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MessagePipeTest" Startup="Application_Startup" > <Application.Resources> </Application.Resources> </Application>
App.cs
using System; using System.Windows; using MessagePipe; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; namespace MessagePipeTest2 { public partial class App : Application { IHost host; private void Application_Startup(object sender, StartupEventArgs e) { host = CreateHostBuilder().Build(); var vm = host.Services.GetRequiredService<MainWindowVM>(); var w = new MainWindow(); w.DataContext = vm; w.Closed += (sender, e) => host.Dispose(); w.Show(); host.RunAsync(); } static IHostBuilder CreateHostBuilder() { return Host.CreateDefaultBuilder() .ConfigureServices((ctx, services) => { services.AddMessagePipe(); services.AddHostedService<Worker.Worker>(); services.AddSingleton<MainWindowVM>(); }); } } }
MainWIndow.xaml
<Window x:Class="MessagePipeTest2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MessagePipeTest2" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <StackPanel> <TextBlock Text="{Binding Value}" /> <TextBox Background="Gray" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" Height="400" Text="{Binding Text}" TextChanged="tbox1_TextChanged"/> </StackPanel> </Window>
MainWIndow.cs
using System.Windows; using System.Windows.Controls; namespace MessagePipeTest2 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void tbox1_TextChanged(object sender, TextChangedEventArgs e) { ((TextBox)sender).ScrollToEnd(); } } }
MainWindowVM
using System; using MessagePipe; using Prism.Mvvm; namespace MessagePipeTest2 { class MainWindowVM : BindableBase { private int _value; public int Value { get { return _value; } set { SetProperty(ref _value, value); } } private string _text; public string Text { get { return _text; } set { SetProperty(ref _text, value); } } ISubscriber<MyEvent> subscriber; readonly IDisposable disposable; public MainWindowVM(ISubscriber<MyEvent> subscriber) { this.subscriber = subscriber; var bag = DisposableBag.CreateBuilder(); this.subscriber.Subscribe(x => { Value = x.Value; Text = x.Text; }).AddTo(bag); disposable = bag.Build(); } void Close() { disposable.Dispose(); } } }
MyEvent.cs
namespace MessagePipeTest2 { public class MyEvent { public int Value { get; set; } public string Text { get; set; } } }
Worker.cs
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using MessagePipe; namespace MessagePipeTest2 { class Worker : BackgroundService { IPublisher<MyEvent> publisher; public Worker(IPublisher<MyEvent> publisher) { this.publisher = publisher; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { int i = 0; string s = ""; while (!stoppingToken.IsCancellationRequested) { publisher.Publish(new MyEvent() { Value = i, Text = s, }); i++; s += DateTime.Now.ToString() + "\n"; await Task.Delay(1, stoppingToken); } } } }
Cysharp/ZString を試してみた
Cygames の子会社 Cy# さんが出してる C# のOSSライブラリ。メモリ消費量が少なく、早い(?)
細かい内容は上の Cy# さんのブログに書かれています。
試したコード
using System; using System.IO; using Cysharp.Text; namespace ZStringTestDotnetCore { class Program { static void Main(string[] args) { var ts1 = MeasureTask(ZStringConcat); var ts2 = MeasureTask(MemoryStreamStringFormat); var ts3 = MeasureTask(ZStringFormatUtf8); var ts4 = MeasureTask(ZStringFormatUtf16); var ts5 = MeasureTask(MemoryStreamZStringFormat); Console.Write($"- ZString(Concat) {ts1}\n"); Console.Write($"- Memory Stream 1 {ts2}\n"); Console.Write($"- ZString(Format) A {ts3}\n"); Console.Write($"- ZString(Format) B {ts4}\n"); Console.Write($"- Memory Stream 2 {ts5}\n"); } static TimeSpan MeasureTask(Action func) { var sw = new System.Diagnostics.Stopwatch(); sw.Reset(); sw.Start(); func(); sw.Stop(); return sw.Elapsed; } static void ZStringConcat() { int i = 0,j = 0,k = 0; using (var sb = ZString.CreateUtf8StringBuilder()) { while (i < 300000) { var now = DateTime.Now; sb.Append(ZString.Concat("i:", i, ",j:", j , ",k:", k, "\n")); i++; j += 2; k += 3; } sb.WriteToAsync(Console.OpenStandardOutput()).Wait(); } } static void MemoryStreamStringFormat() { int i = 0,j = 0,k = 0; using(var ms = new MemoryStream()) { using (var wr = new StreamWriter(ms)) { while (i < 300000) { var now = DateTime.Now; wr.Write(string.Format("i:{0},j:{1},k:{2}\n", i, j, k)); i++; j += 2; k += 3; } wr.Flush(); ms.WriteTo(Console.OpenStandardOutput()); } } } static void ZStringFormatUtf8() { int i = 0, j = 0, k = 0; using (var sb = ZString.CreateUtf8StringBuilder()) { while (i < 300000) { var now = DateTime.Now; sb.Append(ZString.Format("i:{0},j:{1},k:{2}\n", i, j, k)); i++; j += 2; k += 3; } sb.WriteToAsync(Console.OpenStandardOutput()).Wait(); } } static void ZStringFormatUtf16() { int i = 0, j = 0, k = 0; using (var sb = ZString.CreateStringBuilder()) { while (i < 300000) { var now = DateTime.Now; sb.Append(ZString.Format("i:{0},j:{1},k:{2}\n", i, j, k)); i++; j += 2; k += 3; } Console.WriteLine(sb.ToString()); } } static void MemoryStreamZStringFormat() { int i = 0, j = 0, k = 0; using (var ms = new MemoryStream()) { using (var wr = new StreamWriter(ms)) { while (i < 300000) { var now = DateTime.Now; wr.Write(ZString.Format("i:{0},j:{1},k:{2}\n", i, j, k)); i++; j += 2; k += 3; } wr.Flush(); ms.WriteTo(Console.OpenStandardOutput()); } } } } }
結果
- ZString(Concat) 00:00:02.9890532 - Memory Stream 1 00:00:02.6363530 - ZString(Format) A 00:00:02.6669643 - ZString(Format) B 00:00:03.3741640 - Memory Stream 2 00:00:02.6118988
私の試したコードではあまり早くなりませんでした。MemoryStream + StreamWriter のほうがちょっと早いかなという感じ。ここでは記載していませんが、メモリ消費量もあまり違いがない感じでした。もしかすると、ZString は Unity との親和性に重きを置いているので、もしかするとこういうコードではあまり恩恵は受けれないのかもです。
ただ、StringBuilder と比較すると WriteToAsync() メソッドがあって便利ですし、 UTF8用のBuilderもあるので、そちらも便利そうです。
Flutter に入門したい
あらすじ
Flutter がラズパイでも動作することを知り、何か IoT 的なものが作れないかを検討したいので、 Flutter に入門してみようと思う。
チュートリアル
Google CodeLabs の 「初めての Flutter アプリの作成 」 の パート1 と パート2 はとっかかりとしてはよさそうだったし、日本語だったのでそちらをやってみた。
codelabs.developers.google.com
codelabs.developers.google.com
時計の表示
現在時刻をリアルタイムで表示するにはどうしたらいいか。というのが気になったので、以下を参考に作成。
今後
このあたりを見ながら、見繕って学習していきたい。