はじめに
特定の条件において、実装側をC言語で、可視化をC++でのようにコンポーネント分割したい状況があったので、その場合に活用できる実装方法をまとめます。
パフォーマンス重視・移植性重視のアルゴリズム実装では、 C言語を選びたい場面は今でも多くあります。一方で、可視化・デバッグ・UI まで含めると OpenCV をはじめとした C++ライブラリの表現力 は非常に魅力的です。
ここでは、
- 計算ロジックはC
- 可視化はC++(OpenCV)
-
両者を
extern "C"で接続
という構成で、可視化付きシミュレーションを構築する方法を解説します。
この構成は単なる言語混在テクニックではなく、
- C資産を保ったままの可視化
- アルゴリズムとUIの責務分離
といった点でも実用性の高いパターンだと考えています。
目的
- Cで実装した粒子シミュレーションを動かす
- 各フレームの状態を C++ 側に渡す
- OpenCV ウィンドウでリアルタイム描画する
extern "C"とは
cとc++の異なる点
C++では、関数名に引数型などを含めた名前修飾が行われます。
void draw(int);
void draw(double);
これが可能なのは、コンパイラが内部的に異なる名前を生成しているからです。
一方、C言語には名前修飾がありません。
そのため C++関数をそのままCから呼ぶことはできません。
そこで使うのが extern "C" です。
extern "C" {
void foo(int);
}
この指定により、名前修飾を行わず、C互換のリンケージになることで、結果として Cからリンク可能な関数 になります。
実装ステップ
1. プロジェクト構成例
.
├── main.c # C: シミュレーションロジック
├── particle.h # C/C++共有データ構造
├── visualizer.h # C向けインターフェース
├── visualizer.cpp # C++: OpenCV描画
└── Makefile
2. 共有データ構造(particle.h)
C/C++の両方から使うため、純C構文のみで定義します。
#ifndef PARTICLE_H
#define PARTICLE_H
typedef struct {
double x;
double y;
} Particle;
#endif
3. C++側の描画クラス(visualizer.cpp)
OpenCVを用いた描画処理は、C++のクラスに閉じ込めます。
#include <opencv2/opencv.hpp>
#include "particle.h"
class Visualizer {
std::string window;
cv::Mat image;
public:
Visualizer(const char* name, int w, int h)
: window(name), image(h, w, CV_8UC3) {
cv::namedWindow(window);
}
void update(Particle* p, int n) {
image.setTo(cv::Scalar(0, 0, 0));
for (int i = 0; i < n; ++i)
cv::circle(image, cv::Point(int(p[i].x), int(p[i].y)), 4, cv::Scalar(0, 255, 0), -1);
cv::imshow(window, image);
}
~Visualizer() {
cv::destroyWindow(window);
}
};
この時点では Cからは一切見えません。
4. C向けブリッジAPI(visualizer.h)
C側には 不透明ポインタ(Opaque Pointer) だけを公開します。
#ifndef VISUALIZER_H
#define VISUALIZER_H
#include "particle.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef void* VisualizerHandle;
VisualizerHandle visualizer_init(const char* name, int w, int h);
int visualizer_update(VisualizerHandle, Particle*, int);
void visualizer_close(VisualizerHandle);
#ifdef __cplusplus
}
#endif
#endif
Step 5: ブリッジ関数の実装(visualizer.cpp)
#include "visualizer.h"
extern "C" {
VisualizerHandle visualizer_init(const char* name, int w, int h) {
return new Visualizer(name, w, h);
}
int visualizer_update(VisualizerHandle h, Particle* p, int n) {
auto* v = static_cast<Visualizer*>(h);
v->update(p, n);
return cv::waitKey(10) == 'q' ? -1 : 0;
}
void visualizer_close(VisualizerHandle h) {
delete static_cast<Visualizer*>(h);
}
}
Step 6: C側メインロジック(main.c)
#include <stdlib.h>
#include "visualizer.h"
#define N 100
#define W 640
#define H 480
int main() {
Particle p[N];
for (int i = 0; i < N; i++) {
p[i].x = rand() % W;
p[i].y = rand() % H;
}
VisualizerHandle viz = visualizer_init("Simulation", W, H);
while (1) {
for (int i = 0; i < N; i++) {
p[i].x += (rand() % 3 - 1) * 2;
p[i].y += (rand() % 3 - 1) * 2;
}
if (visualizer_update(viz, p, N)) break;
}
visualizer_close(viz);
}
C側からは OpenCVの存在すら見えません。
7. Makefile
CC=gcc
CXX=g++
TARGET=particle_sim
C_SRCS=main.c
CPP_SRCS=visualizer.cpp
OBJS=$(C_SRCS:.c=.o) $(CPP_SRCS:.cpp=.o)
CFLAGS=-std=c11 -Wall -I.
CXXFLAGS=-std=c++11 -Wall -I. $(shell pkg-config --cflags opencv4)
LDFLAGS=$(shell pkg-config --libs opencv4)
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $^ -o $@ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $<
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $<
clean:
rm -f $(OBJS) $(TARGET)
まとめ
この構成のポイントは以下です。
-
計算コアはCで完結
-
可視化はC++に隔離
-
extern "C" による安全な接続
-
不透明ポインタによる実装隠蔽
この設計は、「CとC++を混ぜる」のではなく、役割を分けて接続するという発想で使ってえるかと思います。