mem: 할당·재할당·레이아웃
바이트 단위 크기, 할당 실패, 재할당 경계와 해제 책임을 설명합니다.
Wave Foundation
할당과 해제
std::mem::alloc
mem_alloc(size: i64) -> ptr<u8>
mem_alloc_zeroed(size: i64) -> ptr<u8>
mem_free(p: ptr<u8>, size: i64) -> i64
mem_realloc(old_ptr: ptr<u8>, old_size: i64, new_size: i64) -> ptr<u8>
크기는 바이트 단위입니다. 0 이하 크기의 할당은 null을 반환합니다. 양수 크기도 할당에 실패하면 null일 수 있습니다. mem_alloc의 초기 내용을 가정하지 말고, 0 초기화가 필요하면 mem_alloc_zeroed를 사용합니다.
성공한 할당은 호출자가 소유합니다. 해제에는 원래 할당 크기를 전달합니다. mem_free(null, size)는 0을 반환하고, null이 아닌 포인터와 0 이하 크기의 조합은 오류입니다. 이미 해제한 포인터를 다시 해제하거나 접근하지 않습니다.
재할당의 경우 구분
| 요청 | 동작 |
|---|---|
| 새 크기가 양수이고 성공 | min(old_size, new_size) 바이트를 복사하고 이전 할당 해제 |
| 양수 크기의 새 할당 실패 | null 반환, 기존 할당 유지 |
old_ptr == null, 양수 새 크기 |
새 할당처럼 동작 |
new_size == 0 |
유효한 이전 할당의 해제를 시도하고 null 반환 |
| 음수 크기 또는 기존 포인터에 old_size=0 | null 반환 |
0 크기로 재할당한 결과만으로 해제 성공 여부를 확인할 수 없습니다. 해제 결과를 확인해야 한다면 mem_free를 직접 호출하십시오. 재할당 후 새로 늘어난 영역은 직접 초기화합니다.
기존 포인터를 보존하는 예제
아래는 새 크기가 양수인 경우입니다. main.wave로 저장해 실행합니다.
import("std::mem::alloc")::{
mem_alloc, mem_realloc, mem_free
};
fun main() -> i32 {
var data: ptr<u8> = mem_alloc(4);
if (data == null) {
return 1;
}
deref data[0] = 7;
var grown: ptr<u8> = mem_realloc(data, 4, 8);
if (grown == null) {
mem_free(data, 4);
return 2;
}
data = grown;
println("{}", deref data[0]);
if (mem_free(data, 8) < 0) {
return 3;
}
return 0;
}
실행 결과:
7
실패 확인 전에 data = mem_realloc(...)로 덮어쓰면 기존 주소를 잃을 수 있습니다. 재할당이 성공하면 이전 주소와 그 내부를 가리키던 포인터는 사용하지 않습니다.
대상 타입의 크기와 정렬
std::mem::layout
size_of<T>() -> u64
align_of<T>() -> u64
두 값은 실행 중인 컴퓨터가 아니라 컴파일 대상의 레이아웃입니다. size_of에는 꼬리 패딩이 포함되며, 값을 생성하거나 평가하지 않습니다. 요소 개수에 크기를 곱할 때는 오버플로를 검사합니다. std::mem::ops의 mem_size_mul_checked와 mem_size_add_checked를 사용할 수 있습니다.
mem_copy는 겹치지 않는 범위, mem_move는 겹칠 수 있는 범위 복사에 사용합니다. 어느 쪽도 포인터만으로 실제 할당 길이를 알아내지 못하므로 호출자가 범위를 보장해야 합니다. 크기가 변하는 바이트 목록은 Buffer로 관리할 수 있습니다.
레이아웃 조회 예제
main.wave로 저장해 실행합니다. 문서에서 다루는 타깃의 i32 크기와 정렬은 각각 4바이트이므로 4 4를 출력합니다. 다른 타입, 특히 구조체와 포인터의 값은 대상별로 확인하십시오.
import("std::mem::layout")::{
size_of, align_of
};
fun main() {
println("{} {}", size_of<i32>(), align_of<i32>());
}
크기 계산에서 오버플로 확인하기
할당 함수에 넘기기 전에 count * element_size가 유효한지 확인해야 합니다. 넘친 값으로 작은 공간을 할당하고 원래 개수만큼 쓰면 경계를 벗어나게 됩니다.
import("std::mem::ops")::{mem_size_add_checked, mem_size_mul_checked};
fun main() {
var result: i64 = 99;
if (mem_size_mul_checked(3, 4, &result) == 0) {
println("bytes={}", result);
}
if (mem_size_add_checked(9223372036854775807, 1, &result) < 0) {
println("overflow rejected");
}
}
실행 결과:
bytes=12
overflow rejected
실패한 결과를 할당 크기로 쓰지 않습니다. 일반 산술로 계산한 뒤 결과가 음수인지 보는 것만으로 모든 오버플로를 검출할 수도 없습니다. 검사가 필요한 크기 계산에는 처음부터 checked 함수를 사용합니다.
겹치는 복사에는 mem_move
같은 배열의 일부를 뒤로 옮길 때는 입력과 출력 영역이 겹칩니다. mem_copy에 겹치는 범위를 전달하지 말고 mem_move를 사용합니다.
import("std::mem::ops")::{mem_move};
fun main() {
var data: array<u8, 5> = [1, 2, 3, 4, 5];
mem_move(&data[1], &data[0], 4);
for (var index: i32 = 0; index < 5; index += 1) {
println("{}", data[index]);
}
}
실행 결과:
1
1
2
3
4
원래 앞의 네 바이트가 한 칸 뒤로 이동합니다. 직접 앞에서부터 대입하면 이미 덮어쓴 값을 다시 읽어 전부 1이 되는 실수를 할 수 있습니다. 겹침을 처리하는 API를 선택하면 방향 선택을 직접 구현할 필요가 없습니다.
소유권을 전달하는 함수 작성
메모리를 반환하는 함수는 성공 시 반환 주소와 해제에 필요한 크기를 함께 제공하는 편이 좋습니다. 호출자가 그 크기를 추측해야 하면 잘못된 해제가 생기기 쉽습니다. 빌린 주소를 반환하는 함수라면 호출자가 해제하면 안 된다는 점과 원본 수명을 설명합니다.
함수 경계를 넘어서도 할당자 → 소유자 → 해제 지점을 연결해 읽을 수 있어야 합니다. 포인터 변수의 이름이나 타입만으로 소유권이 자동으로 결정되는 것은 아닙니다.