まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:java

このブログの続きです:
16bit PC-DOS 向け Java (サブセット)コンパイラ&VM "dosjava"

先日上記ページで紹介した開発中の 16bit PC-DOS 向け Java である "dosjava" 、この後で開発を進め、実装内容やテストもある程度進んできたので、正式公開を宣言します!
https://github.com/dotnsf/dosjava

2026050700



詳しくは上記 GiHub のページを参照いただきたいのですが、2026/05/26 時点で以下の機能を実装済みです:
・データ型
 * int, long, float, boolean, String
 * 配列(int, long, float, boolean のみ)
・制御構文
 * if/else, while, for, switch, break, return
 * try/catch/finally
・演算子
 * 四則演算、関係(>, >=, <, <=, ==)、論理(&&,||,!)、代入、型キャスト
・特殊クラス
 * File(テキストファイル入出力)
 * Date(日付時刻演算)
 * Math(算術演算)

以下の機能は現時点での制約事項です:
・1ファイル1クラス
・外部ファイル読み込み不可

また、以下の機能については今の時点では実装予定がありません:
・通常の class ファイルとの互換性
・マルチスレッド対応
・ネットワーク機能

samples\ フォルダ以下に動作確認済みのソースコードがあります。こちらを参考に PC-DOS と dosjava の環境を用意の上、以下の手順でコンパイルして実行してみてください。もちろん自分でソースコードを書いていただいても構いません:
(コンパイル)
> djc hello.jav
Compiled: hello.jav -> hello.djc

(実行)
> djvm hello.djc
Hello World!

本音をいうとネットワーク機能は実装したかったのですが(実際に挑戦したのですが)、16bit だととにかく メモリが足りないという問題に直面しました。それ以外でも利用するパケットドライバに依存してしまうので汎用性がなかったりして、、、ちょっとハードルが高すぎる機能だと判断しました。もしかすると将来的に、特定のパケットドライバに依存した上で HTTP クライアントくらいは・・・ と思っています。まあ、でもメモリ不足問題は時間が解決してくれるものではないので、ちょっと厳しいかなあ・・・

次に拡張しようと考えているのはバイナリファイルを扱えるようにすることと例外処理の強化です。今の時点でテキスト読み書き追加はできるのですが、バイナリファイルは未対応なのでその強化と、あと 実行中に Exeception が発生するパターンがまだ限られている(ゼロで割るとか、存在しないファイルを読みにいくとか)ので、その強化をしたいと思ってます。



この連休中に 16bit PC-DOS 向けの Java(正確には Java サブセット)コンパイラとその VM である "dosjava" を生成 AI 併用で作ってみました。まだまだ機能的には未熟ですが、今のタイミングで一度公開しておきます:
https://github.com/dotnsf/dosjava

2026050700

(↑ 細かすぎて伝わらないと思ったので書きますが、Java Duke を 16bit っぽく粗くしてみました)



【"dosjava" とは?】
16bit OS である PC-DOS(DOS/V)上で動く Java のサブセットです。Java は "Write Once, Run Anywhere!"(記述したコードはどの Java 環境でも動く)を提唱していましたが、色々な制約の事情もあり、残念ながらその理念は継承できていません(別環境の javac でコンパイルした .class ファイルを持ってきても動かない、という意味です)。ただ「ソースコードレベルではなるべく修正なしに動く」ことを開発目標の1つにしています。

PC-DOS 環境なので、例えばファイル名にも制約があります(ファイル名8文字以内、拡張子3文字以内)。コンパイラは "djc.exe" で、Java VM は "djvm.exe" ですが、ソースコードは "test.java" という名称を付けることができず "test.jav" のようなファイル名にする必要があります。これを djc.exe でコンパイルすると "test.djc" というファイルになりますが、これが "test.class" に相当するものです("djvm.exe" で実行可能です)。

ちなみに私の開発環境は 64bit の Windows11 で、クロスコンパイラには Open Watcom V2 を使っています。


【「とりあえず動かしてみたい」人向け】
PC-DOS エミュレータである DOSBox を使うことをおススメします(私自身の動作確認でも DOSBox を使っています)。2026-05-07 時点での最新バージョンは 0.74-3 で、かなり枯れて(安定して)いるものです。Windows 環境であればコマンドプロンプトから winget を使って以下のコマンドでインストールすることも可能です:
> winget install -e --id DOSBox.DOSBox

"dosjava" を DOSBox から使えるようにします。端的に言えば "djc.exe" と "djvm.exe" を PC-DOS 上のパスの通ったディレクトリに置くだけでいいのですが、DOSBox の場合はホスト PC の特定フォルダをマウントする機能があるので、以下はその方法で "dosjava" を使えるようにする手順を紹介します。

まず git clone(又は zip ダウンロード&展開など)で dosjava 一式をホスト PC にコピーします(ちなみに Windows 用の git を持っていない場合はこちらからインストールしてください):
> git clone https://github.com/dotnsf/dosjava
(ホームディレクトリ直下に git clone したものとします)

この時点でホームディレクトリ(C:\Users\myname\ とします)の直下に dosjava というフォルダが作られ、一式がコピーされているものとします(ちなみに実行バイナリは dosjava\build\bin\ 以下に djc.exe と djvm.exe があります)。

これを DOSBox から使えるようにマウントします。DOSBox の設定ファイルを編集するため、"dosbox" で検索して "DOSBox 0.74-3 Options" と書かれた箇所をクリックします(上で紹介した winget 以外の方法で DOSBox をインストールした場合はこの方法では設定ファイルを編集できない可能性があるので、インストール先ディレクトリを探すなどして dosbox.conf ファイルを探してください):
2026050701


設定ファイルをテキストエディタで開いた状態になります:
2026050702


一番下までスクロールして、[autoexec] と書かれた箇所を表示します。そしてその下に以下を記載して保存します(先ほどクローンした dosjava フォルダを DOSBox の c: ドライブとしてマウントし、かつ djc.exe や djvm.exe がある build\bin\ ディレクトリにパスを通して c: ドライブに移動する、という内容です):
mount c c:\Users\(ユーザー名)\dosjava
set PATH=%PATH%;c:\build\bin
c:

2026050703

この状態で DOSBox を起動します(dosbox のアプリを開きます):
2026050704


正しく DOSBox が起動できると以下のような画面になります(上で [autoexec] 以下に書いたコマンドが実行されていることを確認します)。ちなみに DOSBox 内では英語キーボード配列になっているので注意してください(ディレクトリ区切り記号である半角\を日本語キーボードで入力するには右上の(SHIFT キーを併用すると "|" になる)キーではなく、右下の(SHIFT キーを併用すると "_" になる)キーを入力する必要がある点に注意してください:
2026050705


いくつかの動作確認済み Java ソースコードが c:\samples\ フォルダ内に格納されています。試しにこれらをコンパイル&実行してみましょう。

まずは DOSBox 内で以下のコマンドを入力して samples\ フォルダ内のファイル一覧を確認します:
c:\>cd samples
c:\samples\>dir

2026050706


例えば "hello.jav" というファイルの内容を確認するには "type hello.jav" と入力します(関数呼び出しのテストを兼ねているので少しだけ複雑化していますが、いわゆる「ハローワールド」の Java ソースコードです):
2026050707


ではこの hello.jav をコンパイル&実行してみます。まずは djc.exe でコンパイルします。"djc hello.jav" と入力します:
c:\samples\>djc hello.jav

2026050708


コンパイルに成功すると上図のように "Compiled: hello.jav -> hello.djc" と表示されます。これは「hello.jav をコンパイルし、結果を hello.djc として保存した」という意味です。この hello.jdc は一般的な Java でいう所の "hello.class" に相当するものです。

コンパイルした "hello.djc" を実行するには Java VM である "djvm.exe" を使います:
c:\samples\>djvm hello.djc

2026050709


成功すると元のソースコードで記述されていたように "Hello World!" という文字が出力されます。

この samples\ フォルダには他にも(現在の dosjava でテスト済みの)多くのサンプル Java ソースコードがあるので、同様に色々試して(コンパイル&実行)みてください。

自分でソースコードを書きたい、という場合は Windows 環境で dosjava ディレクトリ以下にソースコードを作っていただいてもいいのですが、DOSBox 内のあくまで PC-DOS 環境内で記述したい、という場合は別途 16bit PC-DOS 用のスクリーンエディタを用意して(上述のようにそのフォルダもマウントして、パスを通すなどして)利用してください。個人的には 2024 年にオープンソース化された Vz Editor がおススメです。


【(2026-05-07 時点での)仕様】
dosjava は Java と「なるべくソースコード互換」になるよう設計/開発していますが、残念ながら Java との互換性確保の優先順位を下げて実装したものもあります。

具体的には 2026-05-07 時点のビルドでは Java の以下の機能は実装済みです:
  • 型は int, String 、および int 配列
  • 以下の算術演算: +, -, *, /, %, ++, --, +=, -=
  • 以下の比較演算: ==, !=, <, >, <=, >=
  • 以下の論理演算: &&, ||, !
  • 以下の制御構造: if~else~, for ループ, while ループ
  • System.out.println( 文字列または整数 );
  • String 変数には以下のメソッドを実装済み: length(), toUpperCase(), toLowerCase(), equals(), compareTo(), startsWith(), endsWith(), indexOf(), lastIndexOf(), substr()

以下の機能は Java との互換なしに実装済みです("import java.io.File"; は不要):
  • File 関連テキスト読み書き追加関数: File.open(), File.readLine(), File.writeLine(), File.close()

以下の機能は現時点では未実装(実装予定なし)です。16bit では厳しいことに加え、関連機能(例えば浮動小数点演算を実装すると、それらの配列処理や java.lang.Math との互換性などが必要になる)との兼ね合いで優先順位を下げざるを得なかったものとご理解ください:
  • 浮動小数点演算(float, double, Float, Double)
  • ネットワーク処理
  • 日付時刻演算
  • 外部クラスの import 機能

今後の予定では日本語文字列処理やファイル処理のバイナリファイル対応の実装、そして DOSBox ではない PC-DOS での動作確認あたりを考えています。要望などあれば issues から教えてください。プルリクは大歓迎です。


【おまけ】
この dosjava は生成 AI を併用して開発しています。普通のアプリケーションサービスであれば、今のコーディング生成 AI であればソースコード実装だけでなく、ビルド、テストなども実行させ、途中で失敗する場合は原因を考えさせた上で修正→再ビルド→再テスト・・ と一通り動くようになるまでを能動的に行わせることができます(この方法で進めていくらかかるかは別の話だけど)。とても便利なのですが、本プロジェクトのように「開発環境と実行環境が別」な場合、つまりクロスコンパイルが必要なものに対して、生成 AI をどのように使っていくべきか? というのは面白いテーマのように感じています。

今回の dosjava の場合、動作確認には Windows アプリである DOSBox を使っています。残念ながら DOSBox は生成 AI が外から手を出すことができない(毎回設定ファイルを書き換えて初期実行コマンドを変えればできないことはないんだろうけど・・・)ものです。この環境だとコーディングとビルド成功までは生成 AI に任せて、動作確認やテストは手動実行する必要があります。

つまり、
  • とりあえず開発目標を定めて(人間の役割)、
  • その目標のための開発計画を考えさせて(生成 AI の役割)、
  • 開発計画に沿って、ビルドできるようになるまで実装させて(生成 AI の役割)、
  • 動作確認する。期待通りの結果が出なかった場合はエラー内容や現在の挙動内容を使える(人間の役割)
  • 現在の挙動内容をヒントにソースコードの修正プランを考えさせて、修正を実装する(生成 AI の役割)
  • (期待通りの結果になるまで上の繰り返し)
という役割分担で作業する時間が多かったと感じています。図らずも TDD(Test Driven Development : テスト駆動開発)のスタイルで開発することが感覚的にも合っていた、と思いました。


MySQL テーブル内にバイナリデータを格納する場合、blob 型の列を定義することになります。具体的には blob 型にも複数あり、想定される最大サイズに応じて使うことになります:
最大サイズ(バイト)格納するデータの例
tinyblob255(あまり使われない?)
blob65,535(あまり使われない?)
mediumblob16,777,215画像など
largeblob4,294,967,295動画、音声など


格納するデータの内容によって型を決める必要がありますが、例えば画像、音声、動画といったメディア系のファイルを格納しようとすると tinyblob や blob 型では足りないと思われるので、ほぼ mediumblob か largeblob を使うことになると思われます。1データの最大サイズが 16MB を超える想定があるかどうかで使い分けることになります。ただし largeblob でも 4GB が最大となります。

MySQL で mediumblob 型を使う場合のテーブル定義の例としてはこのようになります:
create table images( id int primary key, img mediumblob ); 

上記例では images テーブル内に mediumblob 型の img 列を定義しました。このテーブルにデータを読み書きする Java のサンプルは後述のようになります。なお後述のサンプル共通で getConnection() という関数を使っていますが、これは java.sql.Connection クラスのインスタンスを返す関数で、具体的には以下のような内容となります:
public Connection getConnection(){
  Connection conn = null;
  try{
    Class.forName( "com.mysql.jdbc.Driver" );
    conn = DriverManager.getConnection( "jdbc:mysql://servername/dbname", "user", "pass" );
  }catch( Exception e ){
    e.printStackTrace();
  }

  return conn;
}


まずはデータの書き込み(insert)です。格納したいバイナリデータが byte[] 型の変数 data に格納されている場合、以下のようなコードで上述の images テーブルに insert することができます:
import java.sql.*;
   : 
 
 
try{
  Connection conn = getConnection();
  String sql = "insert into images( id, img ) values ( ?, ? )";
  PreparedStatement stmt = conn.prepareStatement( sql );
 
 
  stmt.setInt( 1, id ); 
  stmt.setBytes( 2, data ); //. data は byte[] 型 
  int r = stmt.executeUpdate(); 

  stmt.close(); 
  conn.close(); 
}catch( Exception e ){ 
  e.printStackTrace(); 
}

一方、読み出し(select)は以下のようになります。ResultSet からバイナリストリームを取得して ByteArrayOutputStream に書き出し、最後に byte[] 型の変数に変換して取り出しています:
import java.sql.*; 
 : 


byte[] r = null; 

try{ 
  Connection conn = getConnection(); 
  String sql = "select img from images where id = " + id;
  Statement stmt = conn.createStatement(); 

  ResultSet rs = stmt.executeQuery( sql );
  rs.first(); 
  InputStream is = rs.getBinaryStream( 1 );
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  byte[] bs = new byte[1024];
  int size = 0;
  while( ( size = is.read( bs ) ) != -1 ){
    baos.write( bs, 0, size );
  } 

  r = baos.toByteArray();  //. byte[] 型に変換してデータを取得

  stmt.close();
  conn.close(); 
}catch( Exception e ){
  e.printStackTrace();
  r = null; 
}

バイナリデータをサービスシステム内のどこに格納するべきか、という設計の問題を考慮した上で使うべきだと思いますが、この方法で MySQL に格納しておけば、MySQL のバックアップを取ればバイナリデータもまとめてバックアップすることができ、同様に MySQL をリストアすればバイナリデータごとリストアできる、というメリットがあります。


久しぶりの Java プログラミングの機会があり、これまた久しぶりにサーブレットを作りました。POST されたデータを受け取って、バイナリデータを生成して、Content-Type をつけてストリームで返す、というものです。

この POST データの受け取り方をどうするかで(少しだけ)迷ったのですが、変数もその型も数も不定で受け取るような仕様だったので、深く考えずに JSON データを受け取ることにしました。普段の Node.js の時は特殊な事情がない限り JSON で受け取ることにしているので、その延長というか「まあイマドキは JSON だよね」くらいに考えていました。

・・・が、これが意外と苦戦。 誰かの参考になれば・・・、と思って、以下実際に作ったサーブレットの実装を紹介します。

まず最初はクライアント(呼び出し)側は普通にこんな感じの実装にしました。jQuery の AJAX を使ってタイムスタンプ値を含む JSON オブジェクトをポストしています。これを受け取って処理するサーブレット(/postdata)を作るのが今回の目的となります:
  :

$.ajax({
  type: 'POST',
  url: './postdata',
  data: { timestamp: ( new Date() ).getTime(), body: 'ハローワールド' },
  success: function( result ){
    console.log( result );
  },
  error: function( err ){
    console.log( 'error' );
  }
});
:

そしてそのサーブレットのコード部分が以下です:
public class PostdataServlet extends HttpServlet {
	
  @Override
  protected void doPost( HttpServletRequest req, HttpServletResponse res )
throws ServletException, IOException {
req.setCharacterEncoding( "UTF-8" ); try{
//. JSON テキストを全部取り出す BufferedReader br = new BufferedReader( req.getReader() ); String jsonText = br.readLine(); jsonText = URLDecoder.decode( json, "UTF-8" ); //System.out.println( jsonText );
//. JSON オブジェクトに変換 JSONParser parser = new JSONParser(); JSONObject jsonObj = ( JSONObject )parser.parse( jsonText );
//. JSON オブジェクトから特性の属性を取り出す
String body = ( String )jsonObj.get( "body" );
: }catch( Exception e ){ e.printStackTrace(); } } }

要は req.getParameter() などを使って特定のパラメータ値を取り出すわけではなく、req.getReader() を使ってポストされてきた全データ(今回の場合は JSON オブジェクトをテキスト化したもの)を受け取る必要があります。そして受け取ったテキストデータを(上記の例であれば JSON-Simple ライブラリを使って)JSON オブジェクト化した上でサーブレット内で取り扱う、という手法です。


・・・単純に JSON データを扱いたかっただけなんだけど、こんなに面倒だったっけ?

 

拙作マンホールマップの隠し(?)機能でも使っている手法の1つを紹介します。

マンホールマップで特定のマンホール画像を表示すると、このような画面になります(PC版の場合):

http://manholemap.juge.me/page.jsp?id=1125001 の例
2018030401


http://manholemap.juge.me/page.jsp?id=157009 の例
2018030402


注目していただきたいのは地図部分ではなくマンホール画像部分です。画像が色の付いた枠(上は青枠、下は赤枠)で囲われていることにお気づきでしょうか?

これ、実は画像全体の色味を表しています。つまり上の画像は「全体的に青っぽい」、下の画像は「全体的に赤っぽい」ことを自動判断して表示しているのです。なんとなく合ってます(よね)。実際のアプリケーションでは赤、青、緑、黄、紫、そしてマジェンタの6種類のいずれかと判断しています。

これをどうやって実現しているか、というのが今回のネタです。


マンホールマップは Java で開発していて、特にこの部分は Java の拡張クラスである ImageIO (javax.imageio.ImageIO)を使っています。具体的にはこのようなコードです:
	    :
  public String GetImageColor( byte[] img ){
    String r = null;
      :

    if( img != null ){
      BufferedImage image = null;

      try{
        InputStream is = new ByteArrayInputStream( img );
        image = ImageIO.read( is );
      }catch( IOException e ){
      }

      if( image != null ){
        //. 画像の大きさをチェック
        int w = image.getWidth();
        int h = image.getHeight();

        //. 1ピクセルずつ色を取り出す
        for( int x = 0; x < w; x ++ ){
          for( int y = 0; y < h; y ++ ){
            //. 特定ピクセルの RGB 情報を取り出す
            int rgb = image.getRGB( x, y );

              :						
          }
        }
      }else{
      }
    }

    return r;
  }

    :

この例では画像バイナリのバイト配列( byte[] img )を引数として受け取る GetImageColor() 関数を定義しています。その中でまず ImageIO.read を使って、画像の BufferedImage を取り出します。

正しく取り出すことができたら、画像の幅と高さをピクセル単位で取得し、2重の for ループ内で1ピクセルずつ BufferedImage.getRGB 関数を実行し、RGB 値を取り出す、という処理を実行しています。これで1ピクセル毎の RGB 値を取り出すことができます。

あとはこの結果から、赤、青、緑、黄、紫、そしてマジェンタのどの色が多く使われているか、、、を調べるわけですが、そのあたりの細かなアルゴリズムは秘密(というか面倒なので省略)、ということで(苦笑)。


ちなみに白と黒が含まれていない理由は「マンホールの色が黒(または白)」というのは「普通過ぎてつまらないから、黒と白を除いて近い色を探す」という工夫をしているからでした。


この方法を応用することで、色んな画像の色情報(RGB値)を1ピクセル毎に取り出して判断することができるようになります。


このページのトップヘ