C++でautomake

概要

automakeを使ってc++makefileを自動生成してみた。 automakeの使い方習得がメインなので、ソースコードは最小限になってます。

環境構築

まずbrew経由でautomakeのインストール。

$  brew install automake

コンパイラはg++を使う。

$ which g++
/usr/bin/g++

ルートディレクトリを作成し(以下/とする)、構成は以下の通り。

└── src
    └── foo
        ├── main.cpp
        ├── myclass.cpp
        └── myclass.hpp

ファイルの準備

ユーザ側ではルートディレクトリでconfigure.acを1つ、各フォルダ下でMakefile.amを準備する必要がある。 今回の場合は/configure.ac, /Makefile.am, foo/Makefile.am, foo/src/Makefile.am の4つのファイルが必要。

configure.ac

ルートでautoscanの実行。 そうするとconfigure.acの元となるconfigure.scan(とautoscan.log)が作成される。

/ $ autoscan 
/ $ ls 
Makefile.am     autoscan.log    configure.scan  src/

configure.scanをconfigure.acに名前変更し、中身を適宜編集。

/ $ mv configure.scan configure.ac
/ $ vi configure.ac

色々オプションがあるようだが、今回の手順では少なくとも以下2点の対応が必要。

  • AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])のパラメータ設定
  • AM_INIT_AUTOMAKEはautomakeのオプション追加
    AM_INIT_AUTOMAKEはautomakeのオプションを指定することができ、ここではforeignオプションを指定。 後述するように、autmakeにinstallオプションを付けて実行すると、実行時に必要なファイルを自動生成してくれるが、 foreignを指定することで、NEWS, COPYING, AUTHORS, ChangeLog, READMEの自動生成を防ぐ。 詳しくはAutotools Mythbusterを参照。
#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ([2.71])
AC_INIT([foo], [0.1], [foo@bar.com])
AM_INIT_AUTOMAKE([foreign])
AC_CONFIG_SRCDIR([src/foo/myclass.hpp])
AC_CONFIG_HEADERS([config.h])

# Checks for programs.
AC_PROG_CXX

# Checks for libraries.

# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.

AC_CONFIG_FILES([Makefile
                 src/Makefile
                 src/foo/Makefile])
AC_OUTPUT

automakeは上記以外にも色々マクロがある。詳しくは例えばこちら

Makefile.am

ここでは最低限の書き方のみ触れる。 詳細はこちら

/Makefile.am

SUBDIRS = src

/src/Makefile.am

SUBDIRS にサブディレクトリ一覧を指定。

SUBDIRS = foo

/src/foo/Makefile.am

bin_PROGRAMSに実行ファイル名、foo_SOURCESに実行ファイル生成に必要なファイル一覧を羅列。

bin_PROGRAMS = exe
exe_SOURCES = myclass.cpp myclass.hpp main.cpp

ここまでユーザ側の準備は完了。

├── Makefile.am
└── src
    ├── Makefile.am
    └── foo
        ├── Makefile.am
        ├── main.cpp
        ├── myclass.cpp
        └── myclass.hpp

automakeの実行

3ファイルの準備ができたらあとはautoreconfを実行するのみ autoreconfを使ってconfigu.hとsrc/Makefile, src/foo/Makefileを生成。installオプションを付けると、必要なファイルを自動生成してくれる。

/ $ autoreconf --install
configure.ac:6: installing './install-sh'
configure.ac:6: installing './missing'
src/foo/Makefile.am: installing './depcomp'
/ $ ls 
Makefile.am     Makefile.in     aclocal.m4      autom4te.cache/ autoscan.log    config.h.in     configure*      configure.ac    depcomp*        install-sh*     missing*        src/

./configureを実行し、Makefileを作成。

/ $ ./configure 

最後にmakeしてあげて、プログラムの実行

/ $ make
/ $ ./src/foo/exe 
Doing something!
ok

参考

ソースコード

  • /src/foo/main.cpp
#include <iostream>
#include <exception>

#include "myclass.hpp"

int main(int, char* []) {
    try {
        N::myclass mc;
        mc.do_something();
        std::cout << "ok" << std::endl;
        return 0;
    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
        return 1;
    } catch (...) {
        std::cerr << "unknown error" << std::endl;
        return 1;
    }
}
  • /src/foo/myclass.hpp
// myclass.h
#ifndef foo_myclass_hpp
#define foo_myclass_hpp

namespace N
{
    class myclass
    {
    public:
        void do_something();
    };

}

#endif
  • /src/foo/myclass.cpp
// header in standard library
#include <iostream> 
// header in local directory
#include "myclass.hpp" 

using namespace N;

void myclass::do_something(){
        std::cout << "Doing something!" << std::endl;
}

SQLAlchemy使ってみた①

概要

既存のデータベースから必要なデータを取得することを目標とする(そのためトランザクションなどは扱わない)。 今回は手始めに環境構築+SQLAlchemyを使ってSakillaデータベースのメタデータ取得を行う。

環境

準備

まず必要なパッケージのインストール。 pythonプログラムからmysqlに接続するにはドライバのインストールも必要なので、mysql公式ドライバのmysql-connector-pythonをインストール。

wd $ pip install  SQLAlchemy=="1.3.0", mysql-connector-python="8.0.31"

またデータベースのサンプルとしてSakila Sample Databaseを使っていく。 直接リンクを叩くなりwget/curlを使うなりでとりあえずデータベースのダウンロード。 zipを展開すると中身こんな感じ。

wd $ ls sakila-db 
sakila-data.sql    sakila-schema.sql  sakila.mwb

Mysqlのローカルサーバーを立ち上げる。

wd $ mysql.server start 

ユーザ名、パスワードなど確認(テスト用なのでrootで勘弁してくれ)

wd $ mysql -u root -p
Enter password: ?
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.31 Homebrew

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>

まずsqlスクリプトを読み込み、sakilaデータベースを作成する。

mysql> source sakila-db/sakila-schema.sql;
mysql> source sakila-db/sakila-data.sql; 

データベースが正しくインストールされていることの確認。

mysql> USE sakila;
Database changed
mysql> SHOW FULL TABLES;
+----------------------------+------------+
| Tables_in_sakila           | Table_type |
+----------------------------+------------+
.....

これでMySQLサーバーの準備は完了。 ちなみにサーバーを終了するには、セッションから抜け、プロセスをkillすればよい。

mysql> quit 
wd $ mysql.server stop

SQLAlchemyでDB接続の確認

ユーザー側が触れるAPIは、CoreとORMがあるが、今回はCoreを使っていく。 Coreによるデータベースへの接続方法は単にcreate_engineを呼び出すだけである。ドキュメントに従いurlを生成し、create_engineに渡せばよい。

MetaDataにengineをバインドすることで、sakilaデータベースのメタデータが取得可能になる。 まずモジュールのインポート

import urllib.parse
from typing import List, Optional

from sqlalchemy import create_engine, MetaData
import toml 

MySQLサーバーアクセスのための諸々の設定(dialect, driver, username, password, host)はとりあえずtomlファイルに書いておき読み込む。 craete_urlメソットでurlを生成。

class SqlSetting:
    """Read sql setting from toml file"""
    def __init__(self, dialect:str, setting_file: str):
        parsed_toml = toml.load(setting_file)
        self.__dialect = dialect
        self.__username = parsed_toml[dialect]['username']
        self.__password = urllib.parse.quote_plus(parsed_toml[dialect]['password']) 
        self.__host = parsed_toml[dialect]['host']
        self.__driver = parsed_toml[dialect]['driver']
        self.__database = parsed_toml[dialect]['database']

    def create_url(self, dbname: str): 
        return f'{self.__dialect}+{self.__driver}://{self.__username}:{self.__password}@{self.__host}/{dbname}'

create_eingineにurlを渡し、ローカルサーバーのSakilaデータベースにアクセス。 echo=Trueを指定すると、ログが出力され、発行されるsql文を確認することができる。

    setting = SqlSetting(dialect='mysql', setting_file="setting.toml")
    engine=create_engine(setting.create_url(dbname='sakila'), echo=True)

データベースのメタデータを取得するにはMetaDataをインスタンス化し、engineとバインドさせれば良い。

    # bind engine to metadata
    metadata = MetaData(bind=engine)  
    metadata.reflect(only=["actor"]) # onlyを指定すると特定のテーブルのみクエリ(mysqlならshow create table)が発行される
    # show tables 
    for table in metadata.sorted_tables:
        print("table name: ", table.name)
        print("table columns: ", table.columns)
    #>> table name: actor
    #>> table columns:  ['actor.actor_id', 'actor.first_name', 'actor.last_name', 'actor.last_update']
    #>> ...
    #>>table name store
    #>>able columns ['store.store_id', 'store.manager_staff_id', 'store.address_id', 'store.last_update']

例としてactorテーブルのメタデータを取得してみる。

    actor_table = metadata.tables["actor"] 
    print(actor_table.columns)
    #>> ['actor.actor_id', 'actor.first_name', 'actor.last_name', 'actor.last_update']

カラムには下記方法でもアクセス可能。

    print(actor_table.c.actor_id)
    #>> actor.actor_id

次はselect文使ってデータ取得してみる。

WrapperクラスとTemplate化

c++でWrapperクラス・Template化の必要性

 例えば、あるクラスXにもう一つのクラスYのオブジェクトをメンバ変数として持たせたいとする。その場合、元のクラスYのオブジェクトに影響を及ぼさないように、クラスXの初期化の際に、クラスYのオブジェクトをdeep copyしてメンバ変数として持たせる。deep copyはnew演算子により行われるので、deleteにより明示的にメモリを解放する(クラスXのデストラクタをオーバーロードする)必要がある。そうしなければ、クラスXのオブジェクトがスコープを抜けた際にdeep copyしたクラスYのメモリが解放されず、メモリを圧迫することになる(メモリリークにつながる)。そのためクラスX(とその継承クラス)にデストラクタを実装し、毎回クラスYのオブジェクトを明示的にメモリ解放する必要がある。しかしこれではクラスXを継承する度に毎回余分な実装をする必要が出てきて、Open for Extension Closed for modificationのルールに反する。そこで解決策として、クラスYのWrapperクラス(以下WrapperYクラス)を用意することが考えられる。WrapperYクラスにクラスYのメモリハンドリングを任せておき、クラスXからはこのWrapperYを呼ぶことで、クラスXのオブジェクトがスコープを抜けた際に、WrapperYクラスのデストラクタが呼び出され、クラスYオブジェクトのメモリが自動で解放される。このようにWrapperクラスはメモリハンドリングが可能で余分な実装を減らすことに役立つ。  Wrapperクラスの役割は上記がメインのため、あるクラスに対するWrapperクラスの実装は毎度同じようなものになる。例えば、クラスXに、クラスY、クラスZのオブジェクトをメンバ変数として持たせたいとする。その際、WrapperYクラスとWrapperZクラスを用意すればいいのだが、ほぼ同じ実装となるため毎回Wrapperクラスの実装を書くのは煩わしい。そこで役に立つのがTemplateである。Templateを実装すればtemplate<classY> 、template<classZ>のように呼び出すことで、WrapperY、WrapperZを実装した場合と同様の挙動(メモリハンドリング)が可能となる。

rule of three

もしcopy constructor, copy assignment operator, destructorのうち一つでも必要であれば3つ全部必要であるというルール。これは単にcopy constructor, copy assignment operatorを使う時ってdeep copyが必要になるわけやから、オブジェクトをコピーする際には、コンストラクト時にnew演算子を呼ぶわけで、そうなるとdestructorもオーバーロードしてdelete演算子を呼び出す必要がある。逆も然り。