Odroid 개발보드에서 Zig와 C를 같이 사용하기
Odroid-N2+ 개발 보드를 구입해서 네트워크 연결을 관리하는 프로그램을 작성하는 실험을 해보고 있다. 개발 보드에 설치된 우분투 리눅스에서 블루투스나 ZigBee 등 네트워크 프로그래밍을 어떻게 하는지 방법을 찾아보고 있다.
제일 먼저 리눅스의 블루투스 서브시스템인 BlueZ를 사용해보고 싶었다. ZigBee 도 사용해보고 싶은데 ZigBee 동글과 센서를 알리에서 주문했더니 배송이 꽤 걸린다고 해서 블루투스 먼저 시도해봤다.
지금 사용하는 N2+ 보드는 성능이 꽤 나오는 보드이지만, 더 성능이 제한되는 소형 보드에서도 실행 가능한 프로그램이면 좋겠어서 C나 C++의 성능이 나오는 프로그래밍 언어를 찾아보았다.
Zig를 선택하고 빌드 시스템 구축하기
제일 먼저 내가 익숙해하면서도 빠른 개발이 가능한 Python을 검토해보았다. Python도 저수준의 프로그래밍이 가능하지만, 파이썬에서 BlueZ를 사용할 수 있는 pybluez 프로젝트가 개발이 중단되어 있는 상태라 Python은 제외하였다.
C나 C++를 사용해야 싶었지만 언어의 개발 경험이 좋지도 않고 최근 C나 C++을 대체할 수 있는 언어가 많이 나와있기 때문에 대체 언어들을 찾아보았다.
제일 먼저 살펴본 언어는 그 유명한 Rust다. Rust는 C와 상호운용이 가능하면서도 메모리 안전한 프로그램을 작성하고, 성능도 준수한 프로그램을 작성할 수 있다. 그런데 C로 작성된 BlueZ 라이브러리를 사용하려면 C로 작성된 프로그램과 Rust로 작성한 프로그램을 링킹하는 과정이 필요하다. 이러면 Rust 컴파일러와 C 컴파일러를 같이 사용하면서 빌드 시스템이 복잡해질 수 있기 때문에 일단 여기까지 조사하고 넘어갔다.
그 다음 살펴본 언어는 Zig 다.
Zig는 Rust처럼 C 스타일 프로그래밍 언어로, 메모리 안전성과 명시적인 제어 흐름, 컴파일시간 계산이 특징인 언어다.
Zig의 강점은 C, C++ 등 다른 언어로 작성된 프로그램과 상호운용이 아주 쉽다는 점이다.
Zig의 빌드 시스템(zig build
)을 이용하면 C, C++로 작성된 언어를 쉽게 Zig 프로그램과 통합할 수 있다.
프로그래밍하는 것처럼 빌드 시스템을 꾸릴 수 있기 때문에 빌드 시스템을 구축하는 것도 어렵지 않다.
Zig 프로젝트와 BlueZ 통합하기
BlueZ 라이브러리를 Zig 코드와 통합해보자.
일단 BlueZ » Download 에서 유저 공간에서 사용할 수 있는 BlueZ 패키지를 내려받고 프로젝트에 위치시킨다.
프로젝트 구조는 다음과 같다. (tree -L 2
)
.
|-- build.zig
|-- libs
| `-- bluez-5.66
| |-- ...
| |-- lib
| |-- src
| |-- test
| |-- tools
| `-- unit
|-- src
|-- bluetoothlib.zig
`-- main.zig
libs 폴더를 두어 여기에 bluez-5.66
패키지를 위치시켰다. BlueZ에서 제공하는 라이브러리를 사용할 것이므로 bluez-5.66/lib
에 있는 bluetooth.c
, uuid.c
, sdp.c
, hci.c
를 컴파일해야 한다.
이 소스코드들은 bluez-5.66
에 있는 헤더파일들에 선언된 프로토타입과 전처리 매크로가 필요하므로 컴파일 옵션에 -I
로 추가한다.
또한 BlueZ는 libc에 의존성이 있으므로 exe.linkLibc()
를 통해 링킹해준다.
libc의 함수들을 사용할 수 있도록 /usr/include
또한 -I
옵션으로 포함시켜준다.
Zig의 빌드시스템에는 암묵적으로 libc가 포함되지 않아 모두 명시적으로 링킹해주어야한다.
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "opera",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
exe.linkLibC();
exe.addCSourceFiles(&.{
"./libs/bluez-5.66/lib/bluetooth.c",
"./libs/bluez-5.66/lib/hci.c",
"./libs/bluez-5.66/lib/uuid.c",
"./libs/bluez-5.66/lib/sdp.c",
}, &.{
"-Ilibs/bluez-5.66",
"-I/usr/include",
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}
시스템에 연결된 블루투스 디바이스를 찾아 소켓을 여는 간단한 프로그램을 작성해보자.
// main.zig
const std = @import("std");
const btlib = @import("bluetoothlib.zig");
pub fn main() !void {
const dev_id = try btlib.get_route();
const sock = try btlib.get_socket(dev_id);
std.debug.print("dev_id = {d}, sock = {d}\n", .{ dev_id, sock });
}
bluetoothlib.zig
에서는 BlueZ Library에서 정의한 함수에 접근할 수 있도록
C 구조체와 함수의 프로토타입을 지정한다.
그리고 Zig 스타일의 함수로 감싸 퍼블릭 함수로 제공한다.
// bluetoothlib.zig
// C API for the Linux Bluetooth stack
/// Bluetooth device address is a contiguous 6-byte array
pub const bdaddr_t = extern struct {
b: [6]u8 align(1),
};
extern "c" fn hci_get_route(bdaddr: ?*bdaddr_t) c_int;
extern "c" fn hci_devid(str: *const u8) c_int;
pub fn get_route() !i32 {
const dev_id = hci_get_route(null);
if (dev_id < 0) {
return BluetoothError.AdapterNotFound;
}
return @as(i32, dev_id);
}
pub fn get_socket(dev_id: i32) !i32 {
const sock = hci_open_dev(dev_id);
if (sock < 0) {
return BluetoothError.CantOpenSocket;
}
return @as(i32, sock);
}
크로스 컴파일 개발 환경 만들기
내 랩탑은 x86 리눅스이므로 zig build
를 실행하면 x86-64-linux-gnu
로 컴파일된다.
컴파일 속도는 내 랩탑이 빠르므로 내 랩탑에서 빌드하고 그 결과를 개발 보드에서 실행하고 싶다.
따라서 빌드 시에 컴파일 대상을 aarch64-linux-gnu
로 변경한다.
aarch64 리눅스 시스템의 GNU LIBC(GLIBC)를 사용한다는 뜻이다.
다음과 같이 Makefile을 작성해 ssh를 이용해 개발 보드에서 실행해보자.
TARGET_BINARY = ./zig-out/bin/opera
TARGET_MACHINE = /* REDACTED */
.PHONY: build send run connect
all: build send run
build:
zig build -Dtarget=aarch64-linux-gnu
@echo "Build complete."
send:
scp $(TARGET_BINARY) $(TARGET_MACHINE):~
@echo "Binary sent to target machine."
run:
ssh $(TARGET_MACHINE) ~/opera
@echo "Target binary executed on target machine."
connect:
ssh $(TARGET_MACHINE)
>> make
dev_id = 0, sock = 3
잘 된다!
배운 것들
- 실행 파일을 빌드할 때 서로의 프로토타입을 가진 오브젝트끼리 링크해야 한다.
- 프로토타입은 헤더 파일이나
extern ...
등을 이용해 선언할 수 있다.