0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jupyterのcell magicでcellのC/C++ソースをgcc/g++でコンパイルしよう2(Custom magicを作ろう)

Last updated at Posted at 2025-04-19

概要

以前、投稿した下記の記事は%%scriptを使用したcell magicを使って、C/C++ソースをコンパイルできるようにした。これはほとんどプログラミングしないで簡単に実現できたが、機能が限られたものだった。
今回はPythonを用いてCustom Magicを実装して機能を増やしてみることにします。

実行環境

> sw_vers
ProductName:		macOS
ProductVersion:		15.4.1
BuildVersion:		24E263

> python  --version
Python 3.12.7

> jupyter --version
Selected Jupyter core packages...
IPython          : 8.27.0
ipykernel        : 6.28.0
ipywidgets       : 7.8.1
jupyter_client   : 8.6.0
jupyter_core     : 5.7.2
jupyter_server   : 2.14.1
jupyterlab       : 4.2.5
nbclient         : 0.8.0
nbconvert        : 7.16.4
nbformat         : 5.10.4
notebook         : 7.2.2
qtconsole        : 5.5.1
traitlets        : 5.14.3

Custom Magicの作成

Custom Magicのサンプル

サンプルは上記のサイトのものを一部変更して使います。
クラスデコレーションによってmagic classを設定して、関数デコレーションによってcell magic, line magicを区別する。

mymagic.py
import sys
from IPython.core.magic import (Magics, magics_class, line_magic,
                                cell_magic, line_cell_magic)
@magics_class
class MyMagics(Magics):

    @line_magic
    def lmagic(self, line):
        "my line magic"
        print("my line magic example")
        return line

    @cell_magic
    def cmagic(self, line, cell):
        "my cell magic"
        print(cell, end='')
        print(line, file=sys.stderr)
        return line, cell

    @line_cell_magic
    def lcmagic(self, line, cell=None):
        "Magic that works both as %lcmagic and as %%lcmagic"
        if cell is None:
            print("Called as line magic")
            return line
        else:
            print("Called as cell magic")
            return line, cell

def load_ipython_extension(ipython):
    ipython.register_magics(MyMagics)

if __name__ == '__main__':
    load_ipython_extension(get_ipython())

上記のコードでは最後2行はサンプルには載っていないと思いますが、Kernelが立ち上がった時に自動登録するために使います。

それではテストしてみます。Custom Magicを使うためには%load_ext mymagicを使い、コードを変更して再ロードするには%reload_ext mymagicを使います。.pyを付けなように。

mymagic01.png

print(???, file=sys.stderr)で出力するとピンク背景で表示されます。

returnで返した値は入力のcell番号と同じ番号で表示されます。
今回は特に必要ないのでreturnを下記のように削除します。

mymagic.py
import sys
from IPython.core.magic import (Magics, magics_class, line_magic,
                                cell_magic, line_cell_magic)
@magics_class
class MyMagics(Magics):

    @line_magic
    def lmagic(self, line):
        "my line magic"
        print("my line magic example")

    @cell_magic
    def cmagic(self, line, cell):
        "my cell magic"
        print(cell, end='')
        print(line, file=sys.stderr)

    @line_cell_magic
    def lcmagic(self, line, cell=None):
        "Magic that works both as %lcmagic and as %%lcmagic"
        if cell is None:
            print("Called as line magic")
        else:
            print("Called as cell magic")

def load_ipython_extension(ipython):
    ipython.register_magics(MyMagics)

if __name__ == '__main__':
    load_ipython_extension(get_ipython())

前と同じテストをします。returnで返していた値が表示されなくなったのが確認できます。

mymagic02.png

gcc cell magic

簡単なgcc cell magic

まずは簡単なgcc用のCustom Magicを作成します。

gccmagic.py
import os
import sys
import subprocess
from IPython.core.magic import (Magics, magics_class, line_magic,
                                cell_magic, line_cell_magic)

OBJFILE = "./a.out"
COMPILE_EXEC_CMD = "gcc -xc - -o {0} && {0}".format(OBJFILE)

@magics_class
class GCCMagics(Magics):
    
    @cell_magic
    def gcc(self, line, cell):
        """
        gccでコンパイルした後にバイナリの実行
        gcc -xc - -o ./a.out && ./a.out
        """
        rc = self.compile_and_exec(cell)
        print(rc.stdout, end="")
        if rc.stderr != "":
            print(rc.stderr, file=sys.stderr, end="")
    
    def compile_and_exec(self, src):
        rc = subprocess.run(COMPILE_EXEC_CMD, text=True, capture_output=True, shell=True, input=src)
        if os.path.isfile(OBJFILE):
            os.remove(OBJFILE)
        return rc        

def load_ipython_extension(ipython):
    ipython.register_magics(GCCMagics)

if __name__ == "__main__":
    load_ipython_extension(get_ipython())

Magic名を%%gccとしてcellのソースをそのままgccでコンパイルして、エラーがなければ実行する。
コンパイルと実行にはsubprocess.runを使う。コンパイルソースはinput=に指定するとsubprocess.runのコマンドの標準入力に渡される。 そしてsubprocess.runの戻り値には標準出力(stdout)、標準エラー(stderr)があり、それをJupyter上に表示します。そして、生成されたバイナリがあればそれを削除します。
gccのオプション-xcはコンパイルをC言語で、-はソースを標準入力からの指定です。
C++でコンパイルする場合はg++ -xc++とします。
print文でend=""としているのは改行を抑制しています。


テストをします。%%gcc?をするとmagic関数のDocstringが表示されます。コンパイルエラーもエラーなしでの実行結果もちゃんと表示されました。

gccmagic01.png

もっと機能をつけたgcc cell magic

gccmagic.py
import os
import sys
import subprocess
from IPython.core.magic import (Magics, magics_class, line_magic,
                                cell_magic, line_cell_magic)

OBJFILE = "./a.out"
COMPILE_EXEC_CMD = "gcc -xc - -o {0} && {0}".format(OBJFILE)
COMPILE_CMD = "gcc -xc - "

ALL_SOURCE = """
#include <stdio.h>
#include <stdlib.h>
{0}

int main(int argc, char *argv[]) {{

{1}
    
    return 0;
}}    
"""

@magics_class
class GCCMagics(Magics):
    main_source = {}  # main関数内のソース
    other_source = {} # main関数外のソース
    
    @cell_magic
    def gcc(self, line, cell):
        """
        %%gcc コメント
        """      
        rc = self.compile_and_exec(cell)
        print(rc.stdout, end="")
        if rc.stderr != "":
            print(rc.stderr, file=sys.stderr, end="")

    @cell_magic
    def gmain(self, line, cell):
        """
        %%gmain コメント
        """
        src_key = line.strip()
        if src_key != "":
            self.main_source[src_key] = cell                
            rc = self.compile_and_exec(self.make_source())
        else:
            rc = self.compile_and_exec(ALL_SOURCE.format("", cell))           
        print(rc.stdout, end="")
        if rc.stderr != "":
            print(rc.stderr, file=sys.stderr, end="")

    @cell_magic
    def gfunc(self, line, cell):
        """
        %%gfunc コメント
        """
        src_key = line.strip()
        if src_key == "":
            print("lineパラメータがありません", file=sys.stderr)
            return
        else:
            self.other_source[src_key] = cell
        rc = self.compile_and_exec(self.make_source(), exec=False)
        print(rc.stdout, end="")
        if rc.stderr != "":
            print(rc.stderr, file=sys.stderr, end="")

    @line_magic
    def glist(self, line):
        """
        %glist コメント
        """
        print(self.make_source(), end="")

    @line_magic
    def grun(self, line):
        """
        %grun コメント
        """
        rc = self.compile_and_exec(self.make_source())
        print(rc.stdout, end="")
        if rc.stderr != "":
            print(rc.stderr, file=sys.stderr, end="")
        
    def make_source(self, src_key=None):
        other_src = ""
        main_src = ""
        for key in sorted(self.other_source):
            other_src += self.other_source[key]
                
        for key in sorted(self.main_source):
            main_src += self.main_source[key]

        src = ALL_SOURCE.format(other_src, main_src)
        return src
               
    def compile_and_exec(self, src, exec=True):
        if exec:
            rc = subprocess.run(COMPILE_EXEC_CMD, text=True, capture_output=True, shell=True, input=src)
        else:
            rc = subprocess.run(COMPILE_CMD, text=True, capture_output=True, shell=True, input=src)            
        if os.path.isfile(OBJFILE):
            os.remove(OBJFILE)
        return rc        

def load_ipython_extension(ipython):
    ipython.register_magics(GCCMagics)

if __name__ == "__main__":
    load_ipython_extension(get_ipython())
  • %%gcc -- cellのソースをそのままコンパイル&実行
  • %%gmain -- main関数が不要でmain関数を自動で補うので、cellのソースはmain関数内に挿入
    • lineパラメータがない場合はcellのソースだけmain関数を付加してコンパイル&実行
    • lineパラメータがある場合はそれをkeyとしてソースを辞書に登録し、%%gfuncで登録した辞書とここで登録した辞書をkeyのソート順に挿入したフルソースをコンパイル&実行
  • %%gfunc -- main関数の外に挿入。関数やグローバル変数の記述に使う
    • lineパラメータがない場合はエラー
    • lineパラメータがある場合はそれをkeyとしてソースを辞書に登録し、%%gmainで登録した辞書とここで登録した辞書をkeyのソート順に挿入したフルソースをコンパイルのみ
  • %glist -- %%gmainや%%funcで登録したコードにmain関数を付加したフルコードを表示
  • %grun -- %%gmainや%%funcで登録したコードにmain関数を付加したフルコードをコンパイル&実行

テストします。
最初から%reload_extでも良いようです。

gccmagic02.png

gccmagic03.png

gccmagic04.png

%load_extを使わないでCustom Magicの自動登録

以下のディレクトリにgccmagic.pyを移します。startupディレクトリにREADMEがあるのでそれを参考にscript名の前に番号をつけるのも良いと思います。

~/.ipython/profile_default/startup/
README
This is the IPython startup directory

.py and .ipy files in this directory will be run *prior* to any code or files specified
via the exec_lines or exec_files configurables whenever you load this profile.

Files will be run in lexicographical order, so you can control the execution order of files
with a prefix, e.g.::

    00-first.py
    50-middle.py
    99-last.ipy

終わりに

すこし改良の余地があります。

  • %%gmain key で実行するとき、このcellに標準出力が合った場合それだけ出力させる。つまりPythonのようにそのcellだけが実行されたように見せる
    • rc.stdoutを編集すれば出来そう
  • includeやコンパイル時のライブラリの指定
    • includeは%%gfuncでも代用可
  • main関数のテンプレートをcellから変える
  • コンパイルするフルソースを一緒に表示するかのon,off
  • self.main_sourceself.other_sourceのクリアや特定keyの削除
  • ハイライトなどがPythonなので、これをC/C++にしたい
    • たぶんCustom Magicでは出来ないと思う
    • 専用kernelなら可能かも
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?