Paepoi

Paepoi » C and GLib Tips » Clang で C/Objective-c

Clang で C/Objective-c

# 最終更新日 2021.02.27

macOS 環境で CLang を使って C 及び Objective-c 言語のビルド方法と注意点。
なお、macOS の gcc は clang のラッパーです、gcc -v と打てば解ります。
言語そのものは解説しません、本でも買ってください。

あえてコマンドでコンパイルする方法を解説します。
Xcode は Xcode の使い方ばかり勉強するはめになるので使わないほうがいい。

C 言語のビルド
/* testc.c */
#include <stdio.h>

int
main (int argc, char *argv[]) {
	printf("はろーわーるど\n");
	return 0;
}
日本語を出力する何の変哲もない C 言語コードを書き test.c という名で保存。
リテラルに日本語を使う場合は UTF-8 で保存されている必要がある。
コンパイルするには端末を起動してそのディレクトリに cd コマンドで移動します。
clang ファイル名
と打ち込むと gcc 同様に a.out というファイルが作られます。
macOS も UNIX なのでコマンドでは拡張子に意味は無い、単なるデフォルト名。
既に a.out というファイルが存在する場合は注意、容赦なく上書きされます。

コンパイルが成功すると何も表示されず入力待ちに戻ります。
これも gcc と同じ、UNIX ではそれが普通です。

実行パーミッション +x も付いているのでそのまま a.out を実行できる。
ただし当然パスは通っていないので「./」を付加、ドットは現在のパスに変換される。

img2/clang_1.png

文字コードが違う場合もコンパイルは通りますが文字化けします。
Windows からソースコードを持ってきた場合 iconv 等で変換。

コードに間違いがあるとコンパイルエラーになり行番号付きで stderr が出力されます。
以下は printf の最後にセミコロンを入れ忘れた場合、Fedora の gcc とはメッセージは異る。

img2/clang_2.png

コマンドラインオプション
gcc 同様に -o オプションで別の名前を付けられます。
たとえば suzuki.sv という名前にしたければ
clang -o suzuki.sv test_c.c
オプションについては gcc とほぼ同じです、まとめているサイト見つけました。
GCC と Clang のオプション概要 — SOLID 2.0.0 ドキュメント
分割コンパイル
Clang では gcc 同様 の手段ではコンパイルエラーになる。
キチンとヘッダを作りプロトタイプ宣言を行う必要があります。
/* main.h */

/* インクルードガード */
#ifndef SUZUKI_HAYABUSA
#define SUZUKI_HAYABUSA

/* プロトタイプ宣言 */
int plus (int x);

#endif
/* func.c */

#include "main.h"

int plus (int x) {
    return x + x;
}
/* main.c */

#include <stdio.h>
#include "main.h"

int
main (int argc, char *argv[]) {
    printf ( "%d\n", plus(3) );
    return 0;
}
後は gcc func.c main.c のようにソースコードを全部指定する。
インクルードガード、プロトタイプ宣言、extern宣言、の解説はしません。
#pragma once が実は gcc|Clang 共に使えるんですけどね。

C++ 言語のビルド
C++ 言語のコンパイルは clang++ コマンドを使います。
class, STL 等は C++ 標準なので当然 macOS も普通に利用できます。
// stl.cc
#include <iostream>
#include <vector>
#include <algorithm>

/**
 * macOS Big Sur で下記をビルドする場合 -std オプションが必要
 * clang++ -std=c++14 stl.cc
 * or
 * clang++ -std=c++17 stl.cc
 */
template <class T>
struct outputfunc {
    void operator()(T obj) {
        std::cout << obj << " ♪♪♪ ";
    }
};

class App {
public:
    App() {
        std::vector<std::string> vec = {"SUZUKIの", "バイクは", "カッコイイ"};
        // 関数オブジェクトにしたけど今は範囲ベース for のほうがいい
        std::for_each(vec.begin(), vec.end(), outputfunc<std::string>());
    }
    ~App() {
        std::cout << std::endl << "今日も元気でね!" << std::endl;
    }
    static auto init() {
        // auto で this を戻せる
        return new App();
    }
};

int
main(int argc, char* argv[]) {
    // イヂワルな初期化
    auto app = App::init();
    delete app;
    return 0;
}
macOS では C++ ソースの拡張子は cpp が一般的ですが Linux 同様な cc でもいい。
Linux の g++ は拡張子が小文字の c なファイルでもビルドしますが Clang は警告を出す。
ファイル名の大文字小文字を区別しない仕様なので大文字の C を使うのは控えよう。

UNICODE
Clang で wchar_t は gcc と同じ UTF-32(UCS-4) で 4byte である。
ただ macOS の Clang では uchar.h が提供されていないようです。
u|U のプリフィックス指定は対応しているので下記のようにすることは可能。
#include <stdio.h>
#include <wchar.h> // wchar_t
//#include <uchar.h> // char32_t

// C11 対応の Clang にて

int
main (int argc, char const* argv[]) {
    /**
     * char16_t char32_t の確認
     */
    printf("リテラルのサイズは %lu バイト\n", sizeof('a'));
    printf("char のサイズは %lu バイト\n", sizeof(char));
    printf("wchar_t のサイズは %lu バイト\n", sizeof(wchar_t));
    printf("UTF-16(UCS-2) のサイズは %lu バイト\n", sizeof(u'a'));
    printf("UTF-32(UCS-4) のサイズは %lu バイト\n", sizeof(U'a'));
    return 0;
}

/* output
リテラルのサイズは 4 バイト
char のサイズは 1 バイト
wchar_t のサイズは 4 バイト
UTF-16(UCS-2) のサイズは 2 バイト
UTF-32(UCS-4) のサイズは 4 バイト
*/

64bit
macOS は High Sierra を最後に 32bit サポートを打ち切った完全 64bit です。
CPU が M1 になりましたが 64bit のままです、つまり Linux の x86_64 と同じ結果になる。
#include<stdio.h>
#include<stdlib.h>

int
main (int argc, char *argv[]) {
    int * p;
    printf ( "int     = %lu\n", sizeof(int) );
    printf ( "long    = %lu\n", sizeof(long) );
    printf ( "pointer = %lu\n", sizeof(p) );
    printf ( "char    = %lu\n", sizeof(char) );
    printf ( "wchar_t = %lu\n", sizeof(wchar_t) );
    printf ( "size_t  = %lu\n", sizeof(size_t) );
    return 0;
    
    /* output
    Linux x86_64 | macOS M1
                 |
    int     = 4  | int     = 4
    long    = 8  | long    = 8
    pointer = 8  | pointer = 8
    char    = 1  | char    = 1
    wchar_t = 4  | wchar_t = 4
    size_t  = 8  | size_t  = 8
    */
}
Intel Mac でも結果は同じ。

C99|C11
現行 macOS の Clang は標準で C99 に対応しています(// コメント等)
c89|c99 というラッパースクリプトは Clang でも使える。
#include<stdio.h>

// 以下のコマンドにすると C89 準拠になりビルドできない
// clang -std=c89 hoge.c
// c89 hoge.c

int
main (int argc, char *argv[]) {
    // 変数宣言の簡易化
    //int i;
    //for (i=0; i<3; i++) {
    for (int i=0; i<3; i++) {
        printf("%d\n", i);
    }
    return 0;
}
C11 は一部対応、-std=c11 というオプションは無い。
#include <stdio.h>

// C11 test

int main (int argc, char const* argv[]) {
    /**
     * C11: fopen のモードに x 追加
     * 存在するファイルを開けないように指定
     * ちなみに fopen_s は Visual Studio だけ
     */
    FILE *fp = fopen("スズキ.txt", "w");
    if (fp == NULL) {
        printf("書き込みできません\n");
        return -1;
    } else {
        fputs("カッコイイ!", fp);
        fclose(fp);
    }
    fp = fopen("スズキ.txt", "wx");
    if (fp == NULL) {
        printf("存在するので書き換えできません\n");
        // gets の実験はこの return をコメントアウトしてね
        return -1;
    } else {
        fputs("ダサイ!", fp);
        fclose(fp);
    }
    /**
     * gets は削除されていない、警告も Clang では出ない
     * gcc や Clang では fgets 推奨
     * ちなみに gets_s は Visual Studio だけ
     */
    char buf[1024];
    printf( "何か入力してください > " );
    gets(buf);
    printf("入力されたのは「%s」です\n", buf );
    return 0;
}

C++11|C++14|C++17
現行 macOS の Clang ではオプション無しで使える標準は C++98 です。
C++11 までを有効にするには -std=c++11 オプションが必要。
更に -std=c++14, -std=c++17 オプションが使えます。
#include <iostream>
#include <vector>
#include <algorithm>

/**
 * clang++ -std=c++14 test.cpp
 * 拡張子は cc か cpp にしないと警告
 */

int main (int argc, char const* argv[]) {
    /**
     * C++11 auto による型憶測や範囲ベース for ループ
     */
    const char suzuki[3][16] = {"SUZUKIの", "バイクは", "カッコイイ"};
    for (auto x : suzuki) {
        std::cout << x << std::endl;
    }
    /**
     * C++14 ラムダ式の拡張
     */
    std::vector<std::string> v = {"SUZUKIは", "クルマも", "カッコイイ!"};
    std::for_each( v.begin(), v.end(), [] (auto s) -> void {
        std::cout << s << std::endl;
    });
    return 0;
}
C++17 は一部対応、オプションが必要。
#include <iostream>

// C++ 17 構造化束縛、std::pair や std::tuple を Python のように受け取れる
// g++ -std=c++17 test_cc17.cc

std::pair<std::string, int> suzuki() {
  return {"GSR", 400};
}

int main (int argc, char const* argv[]) {
    auto [bike, cc] = suzuki();
    std::cout << bike << cc << std::endl;
    return 0;
}

Trigraph, Digraph
Trigraph は Clang では削除されました、Digraph はオプション無しで使える。
右 Alt キー経由でないとブレースが打てない言語圏への配慮だと思いますけど。
日本語キーボードは恵まれているので使う機会なんて無いですが。
%:include <stdio.h>
 
main() <%
    printf("%s\n", "はろー Digraph");
%>

Makefile
make コマンドは Command Line Tools に含まれています。
make コマンドは Makefile というファイルを読み取り各種コマンドを実行します。
以下の内容で拡張子を付けない Makefile という名前のファイルを作成。
注意としてインデントは \t で行うのを忘れないように。
# Makefile
katana: suzuki.c
    clang -o katana suzuki.c
それを suzuki.c というソースがあるディレクトリに置きましょう。
そして端末から make コマンド、katana という実行ファイルが作成されます。

macOS はファイル名の大小文字を区別しないので makeFile とかの名前でも実はイケる。
他の UNIX では普通に区別するので標準に従いましょう。
Makefileの解説

Objective-c 言語のビルド
Objective-c って実はメチャクチャ簡単です。
C 言語ソースの拡張子を m に変更し clang に渡すだけです。
本当にそれだけでビルドできます、include を import に書き換える必要すら無い。

そりゃなんたって C 言語にメッセージ式等の追加をしただけの言語ですから。
Objective-c がワカランという人は C 言語もできないという自己紹介しているだけ。
脱線失礼、ただそれでは Objective-c を使う意味は無いので。
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>

// clang hello.m -framework Foundation

@interface Zou : NSObject
- (void) sayHello:(NSString *)str
           length:(NSUInteger)len;
@end

@implementation Zou
- (void) sayHello:(NSString *)str
           length:(NSUInteger)len {
    NSLog(@"ゾウの%@の長さは %ldm です。", str, len);
}
@end

int main(int argc, char *argv[]) {
    NSString *abc = @"うんち";
    NSUInteger l = [abc length];
    id zou = [Zou alloc];
    [zou sayHello:abc length:l];
    return 0;
}
-framework オプションでフレームワークの指定が必須です。
interface は通常ヘッダに書きます、後は C 言語が解るならすぐ理解できます。
ってコレだけ見ると Objective-c って面倒なだけに見えますよね。
この言語の醍醐味は Cocoa を使う時に解ります。

NSApp を作る
PyObjC で作った NSApp 同様なものをコードだけで作ってみます。
NSApplication は他の Application クラス実装と違いウインドウの無いアプリが作れます。
説明が逆のような気がするけど、気にしない。

ヘッダ、説明用に単純化したけど実用時は C 言語同様にパーツ毎に分離してね。
#import を使う場合はインクルードガードは不要だそうです。
/* app.h */

#import <Cocoa/Cocoa.h>

@interface AppMenu : NSMenu
@end

@interface AppDelegate : NSObject<NSApplicationDelegate>
@end
実装部、拡張子は m にする。
clang に渡すソースは拡張子 m にした実装部だけでいい。
/* app.m */

#import "app.h"

// clang app.m -framework Cocoa

@implementation AppDelegate
- (id) init {
    [super init];
    return self;
}
- (void) applicationDidFinishLaunching:(NSNotification *)aNotification{
    // アクティブ化
    [NSApp activateIgnoringOtherApps:YES];
}
@end

@implementation AppMenu
- (id) init {
    [super init];
    id item_app = [[NSMenuItem new] autorelease];
    [self addItem:item_app];
    id menu_app = [[NSMenu new] autorelease];
    [item_app setSubmenu:menu_app];
    id item_quit = [[NSMenuItem new] autorelease];
    [item_quit initWithTitle:@"Quit App" action:@selector(terminate:) keyEquivalent:@"q"];
    [menu_app addItem:item_quit];
    return self;
}
@end

int main(int argc, char *argv[]) {
    // ガベージコレクションではないのでコレを利用
    [NSAutoreleasePool new];
    // NSApp を作る
    [NSApplication sharedApplication];
    // C ではコレが必須だった
    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
    // command+Q で終了するメニューを入れる
    id main_menu = [[AppMenu new] autorelease];
    [NSApp setMainMenu:main_menu];
    // Delegate
    id delegate = [[AppDelegate new] autorelease];
    [NSApp setDelegate:delegate];
    // メインループを回す
    [NSApp run];
    //
    return 0;
}
注意点はアクティブ化するのはデリゲート内で行うこと。
main 関数内で行うと処理を抜けきれずメニューが使えなくなる。

こんな感じで Foundation のみの時と違いスッキリしたコードになる。
Windows SDK や GTK+ を C 言語で書いた経験がある人なら衝撃レベルです。
Objective-c は C 言語ベースですが C 言語を使う必要はほぼありません。
Cocoa だけでもイケるけど C の資産も使えるおいしい言語なんです。

Copyright(C) sasakima-nao All rights reserved 2002 --- 2024.