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

# 概要

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

https://qiita.com/h6591/items/22002e688bed4959d9f5

# 実行環境

```terminal
> 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のサンプル

https://ipython.readthedocs.io/en/stable/config/custommagics.html

サンプルは上記のサイトのものを一部変更して使います。
クラスデコレーションによって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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/6b527a61-57c3-4765-b96d-7cf13e8a473c.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/be6d2775-4ef6-4709-b9b1-56ef0979682c.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/887be901-d6be-4c7f-a6ea-a957101c49c2.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/1214fcfc-b6e7-4113-8336-8bb85262c084.png)

![gccmagic03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/c948cf79-14a7-493c-8bdd-0bb4def1ba06.png)

![gccmagic04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/1ece3087-5460-4b71-8e9e-2c2dc4c73224.png)


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

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

```
~/.ipython/profile_default/startup/
```

```text: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_source`、`self.other_source`のクリアや特定keyの削除
- ハイライトなどがPythonなので、これをC/C++にしたい
    - たぶんCustom Magicでは出来ないと思う
    - 専用kernelなら可能かも