├── rebar.lock ├── CHANGELOG ├── .gitignore ├── src ├── emmap.app.src ├── emmap_queue.erl └── emmap.erl ├── Makefile ├── rebar.config ├── test ├── test.cpp └── block_storage_tests.erl ├── .github └── workflows │ └── erlang.yml ├── c_src ├── bs.hpp ├── Makefile ├── bsna.hpp └── emmap.cpp ├── LICENSE └── README.md /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2022-08-21 Add persistent queue and non-backward-compatible change to return value of emmap:open/3. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | _build 3 | deps 4 | .eunit 5 | .rebar 6 | *~ 7 | *.so 8 | *.o 9 | *.d 10 | *.beam 11 | *.crashdump 12 | -------------------------------------------------------------------------------- /src/emmap.app.src: -------------------------------------------------------------------------------- 1 | {application,emmap, 2 | [{description,"Erlang Memory-Mapped Files"}, 3 | {vsn,"2.1.2"}, 4 | {modules,[emmap]}, 5 | {registered,[]}, 6 | {applications,[kernel,stdlib]}, 7 | {env,[]}, 8 | {maintainers,["Serge Aleynikov"]}, 9 | {licenses,["Apache2"]}, 10 | {links,[{"Github","https://github.com/saleyn/emmap"}]}]}. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR=rebar3 2 | 3 | all: deps compile 4 | 5 | compile: 6 | @TERM=dumb $(REBAR) compile 7 | 8 | clean: 9 | @$(REBAR) clean 10 | @make -C c_src clean >/dev/null 2>&1 11 | 12 | deps: 13 | @$(REBAR) get-deps 14 | 15 | check: 16 | @$(REBAR) dialyzer 17 | 18 | doc docs: 19 | @$(REBAR) ex_doc 20 | 21 | test eunit: 22 | @TERM=dumb $(REBAR) eunit 23 | 24 | nif: 25 | make -C c_src 26 | 27 | publish: docs clean 28 | $(REBAR) hex $(if $(replace),publish --replace,cut) 29 | 30 | .PHONY: test doc 31 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | warnings_as_errors, 3 | warn_export_all 4 | ]}. 5 | 6 | {port_specs, [{"priv/emmap_nifs.so", ["c_src/*.cpp"]}]}. 7 | 8 | {ex_doc, [ 9 | {extras, [ 10 | {"README.md", #{title => "Overview"}}, 11 | {"LICENSE", #{title => "License"}} 12 | ]}, 13 | {main, "README.md"}, 14 | {source_url, "https://github.com/saleyn/emmap"} 15 | ]}. 16 | 17 | {pre_hooks, [{compile, "make -C c_src"}]}. 18 | 19 | {post_hooks, [{clean, "rm -fr *.dump *.crashdump _build"}]}. 20 | 21 | {plugins, [rebar3_hex, {rebar3_ex_doc, "0.2.12"}]}. 22 | 23 | {hex, [{doc, ex_doc}]}. 24 | -------------------------------------------------------------------------------- /test/test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | template 6 | size_t block_size(size_t bs, int n = 64) { return 8 + n * block_size(bs); } 7 | 8 | template<> 9 | size_t block_size<0>(size_t bs, int) { return bs; } 10 | 11 | template 12 | void* addr(void* mem, int n, size_t bs) { 13 | return addr((char*)mem + block_size(bs, n % 64), n / 64, bs); 14 | } 15 | 16 | template<> 17 | void* addr<0>(void* mem, int, size_t) { return mem; } 18 | 19 | int main() { 20 | 21 | assert(1 == __builtin_ffsll(0xffff'ffff'ffff'ffff)); 22 | assert(1 == __builtin_ffsll(1)); 23 | assert(2 == __builtin_ffsll(1 << 1)); 24 | assert(3 == __builtin_ffsll(1 << 2)); 25 | assert(31 == __builtin_ffsll(1 << 30)); 26 | assert(32 == __builtin_ffsll(1 << 31)); 27 | assert(33 == __builtin_ffsll(1ul << 32)); 28 | assert(64 == __builtin_ffsll(1ul << 63)); 29 | assert(0 == __builtin_ffsll(0)); 30 | 31 | uint64_t mask = 1ul << 63; 32 | assert(64 == __builtin_ffsll(mask)); 33 | 34 | assert(10 == block_size<0>(10, 3)); 35 | assert(8 + 3 * 10 == block_size<1>(10, 3)); 36 | assert(8 + 15 * (8 + 64 * 10) == block_size<2>(10, 15)); 37 | 38 | static char buff[256]; 39 | void* p = buff; 40 | 41 | assert(p == addr<0>(p, 1, 10)); 42 | assert((char*)p 43 | + 8 44 | + 13 * 10 45 | == addr<1>(p, 13, 10)); 46 | 47 | assert((char*)p 48 | + 8 49 | + 7 * (8 + 64 * 11) 50 | + 8 51 | + 13 * 11 52 | == addr<2>(p, (13 * 64 + 7), 11)); 53 | 54 | return 0; 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | name: OTP ${{ matrix.otp }} ${{ matrix.os }}.${{ matrix.compiler.compiler }} 13 | 14 | container: 15 | image: erlang:${{matrix.otp}} 16 | 17 | strategy: 18 | matrix: 19 | os: [ linux ] 20 | compiler: 21 | - { compiler: GNU-12, CC: gcc-12, CXX: g++-12 } 22 | - { compiler: GNU-11, CC: gcc-11, CXX: g++-11 } 23 | - { compiler: LLVM-12, CC: clang-12, CXX: clang++-12 } 24 | - { compiler: LLVM-11, CC: clang-11, CXX: clang++-11 } 25 | otp: ["25.0.4"] 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Compile 30 | run: make 31 | - name: Dialyzer 32 | run: make check 33 | - name: Test 34 | run: make test 35 | 36 | macOS: 37 | runs-on: ${{ matrix.os }} 38 | name: OTP ${{ matrix.otp }} ${{ matrix.os }}.${{ matrix.compiler.compiler }} 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | os: [ macos-latest ] 43 | compiler: 44 | - { compiler: XCode, CC: cc, CXX: c++ } 45 | 46 | steps: 47 | - uses: actions/checkout@v2 48 | - name: Install Base Dependencies 49 | run: | 50 | brew update > /dev/null || true 51 | brew tap Homebrew/bundle 52 | brew install erlang@25 53 | brew install rebar3 54 | #brew bundle --verbose 55 | - name: Compile 56 | env: 57 | CC: ${{ matrix.compiler.CC }} 58 | CXX: ${{ matrix.compiler.CXX }} 59 | SRC_DIR: ${{ github.workspace }} 60 | BUILD_DIR: ${{ github.workspace }}/build 61 | INSTALL_PREFIX: ${{ github.workspace }}/install 62 | run: make 63 | - name: Dialyzer 64 | run: make check 65 | - name: Test 66 | run: make test 67 | -------------------------------------------------------------------------------- /c_src/bs.hpp: -------------------------------------------------------------------------------- 1 | // ex: ts=2 sw=2 ft=cpp et 2 | /** 3 | * \file 4 | * \brief Fixed size blocks storage 5 | * \author Dmitriy Kargapolov 6 | * \since 30 June 2024 7 | */ 8 | #pragma once 9 | 10 | #ifndef __has_builtin 11 | #error "__has_builtin is not defined" 12 | #endif 13 | 14 | #if not __has_builtin(__builtin_ffsll) 15 | #error "__builtin_ffsll is not defined" 16 | #endif 17 | 18 | struct bs_head { 19 | size_t block_size; 20 | }; 21 | 22 | namespace fsbs { 23 | 24 | template 25 | size_t block_size(size_t bs, int n = 64) { return 8 + n * block_size(bs); } 26 | 27 | template<> 28 | size_t block_size<0>(size_t bs, int) { return bs; } 29 | 30 | template 31 | int alloc(void* mem, size_t bs) { 32 | auto mask_ptr = static_cast *>(mem); 33 | 34 | while (true) { 35 | // find group that not yet filled (marked by 1) 36 | uint64_t mask = mask_ptr->load(std::memory_order_relaxed); 37 | int n = __builtin_ffsll(mask) - 1; 38 | if (n < 0) 39 | return -1; 40 | 41 | // pointer to the group mask 42 | char *p = (char *)mem + block_size(bs, n); 43 | 44 | // find subgroup or free block 45 | int m = alloc(p, bs); 46 | if (m < 0 || m == 63) { 47 | // ensure mask bit cleared to mark group filled 48 | uint64_t new_mask; 49 | do 50 | new_mask = mask & ~(1 << n); 51 | while (!mask_ptr->compare_exchange_weak(mask, new_mask, 52 | std::memory_order_release, std::memory_order_relaxed)); 53 | 54 | // try another group 55 | if (m < 0) continue; 56 | } 57 | 58 | // return index 59 | return m * 64 + n; 60 | } 61 | } 62 | 63 | template<> 64 | int alloc<0>(void* mem, size_t) { 65 | auto mask_ptr = static_cast *>(mem); 66 | uint64_t mask = mask_ptr->load(std::memory_order_relaxed); 67 | 68 | while (true) { 69 | int n = __builtin_ffsll(mask) - 1; 70 | if (n < 0) 71 | return -1; 72 | 73 | uint64_t new_mask = mask ^ (1 << n); 74 | if (mask_ptr->compare_exchange_weak(mask, new_mask, 75 | std::memory_order_release, std::memory_order_relaxed)) 76 | return n; 77 | } 78 | } 79 | 80 | int alloc(void* mem, size_t bs) { 81 | return alloc<2>(mem, bs); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /c_src/Makefile: -------------------------------------------------------------------------------- 1 | CURDIR := $(shell pwd) 2 | BASEDIR := $(if $(patsubst %c_src,,$(lastword $(CURDIR))),$(abspath $(CURDIR)/..),..) 3 | OUTDIR := $(REBAR_BARE_COMPILER_OUTPUT_DIR) 4 | REBAR_ENV ?= $(shell echo "${REBAR_BUILD_DIR}" | sed 's,^.*_build/,,') 5 | 6 | ERTS_INCLUDE_DIR ?= $(shell erl -noshell -noinput -eval "io:format(\"~ts/erts-~ts/include/\", [code:root_dir(), erlang:system_info(version)]), halt(0).") 7 | ERL_INTERFACE_INCLUDE_DIR ?= $(shell erl -noshell -noinput -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, include)]), halt(0).") 8 | ERL_INTERFACE_LIB_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, lib)]), halt(0).") 9 | 10 | SOURCES := $(wildcard *.cpp) 11 | OBJECTS := $(addsuffix .o, $(basename $(SOURCES))) 12 | TARGETS := $(addprefix $(BASEDIR)/priv/, $(addsuffix .so, $(basename $(SOURCES)))) 13 | HEADERS := $(wildcard *.h*) 14 | 15 | ifneq ($(OUTDIR),$(BASEDIR)) 16 | ifneq ($(OUTDIR),) 17 | TARGETS += $(addprefix $(OUTDIR)/priv/, $(addsuffix .so, $(basename $(SOURCES)))) 18 | endif 19 | endif 20 | 21 | all: $(TARGETS) 22 | 23 | ifneq ($(or $(findstring $(NIF_DEBUG),1 true),$(findstring test,$(REBAR_ENV))),) 24 | ADDFLAGS := -DDEBUG -O0 25 | else 26 | ADDFLAGS := -O3 -DNDEBUG 27 | V := 0 28 | endif 29 | 30 | # If BS_LEVELS is defined, add a macro definition for it 31 | ifeq ($(origin BS_LEVELS), environment) 32 | ADDFLAGS += -DBS_LEVELS=$(BS_LEVELS) 33 | endif 34 | 35 | UNAME_SYS := $(shell uname -s) 36 | 37 | ifeq ($(UNAME_SYS),Darwin) 38 | CXX ?= cc++ 39 | LDFLAGS ?= -flat_namespace -undefined suppress 40 | else ifeq ($(UNAME_SYS),FreeBSD) 41 | CXX ?= cc++ 42 | else # ifeq ($(UNAME_SYS),Linux) 43 | CXX ?= g++ 44 | endif 45 | 46 | CXXFLAGS += $(ADDFLAGS) $(INC_DIR) -std=c++17 -Wall 47 | 48 | CXXFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) 49 | 50 | LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) -lei -ldl 51 | LDFLAGS += -shared 52 | 53 | # Verbosity. 54 | 55 | c_verbose_0 = @echo " C " $(?F); 56 | c_verbose = $(c_verbose_$(V)) 57 | 58 | cpp_verbose_0 = @echo " CPP " $(?F); 59 | cpp_verbose = $(cpp_verbose_$(V)) 60 | 61 | link_verbose_0 = @echo " LD " $(@F); 62 | link_verbose = $(link_verbose_$(V)) 63 | 64 | COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c 65 | 66 | $(BASEDIR)/priv/%.so: %.o | $(BASEDIR)/priv 67 | $(link_verbose) $(CXX) $< $(LDFLAGS) $(LDLIBS) -o $@ 68 | 69 | $(BASEDIR)/priv: 70 | @mkdir -p $@ 71 | 72 | %.o: %.cpp $(HEADERS) 73 | $(COMPILE_CPP) $(OUTPUT_OPTION) $< 74 | 75 | ifneq ($(BASEDIR),$(OUTDIR)) 76 | ifneq (,$(OUTDIR)) 77 | $(OUTDIR)/priv/%.so: $(BASEDIR)/priv/%.so | $(OUTDIR)/priv 78 | @if [ "$(shell readlink -f $<)" == "$(shell readlink -f $@)" ]; then \ 79 | echo "Skip copying file to $@: source and destination are the same"; \ 80 | else \ 81 | cp -vf "$<" "$@"; \ 82 | fi 83 | 84 | $(OUTDIR)/priv: 85 | @mkdir -p $@ 86 | endif 87 | endif 88 | 89 | .PRECIOUS: $(OBJECTS) 90 | 91 | clean: 92 | @rm -f $(TARGETS) $(OBJECTS) *.d 93 | -------------------------------------------------------------------------------- /test/block_storage_tests.erl: -------------------------------------------------------------------------------- 1 | -module(block_storage_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | not_on_mac_test() -> 6 | case os:type() of 7 | {unix, darwin} -> 8 | {skip, "Skipping macOS-incompatible tests"}; 9 | _ -> 10 | [ 11 | basic_test_t(), 12 | big_random_test_t(), 13 | block_storage_test_t() 14 | ] 15 | end. 16 | 17 | basic_test_t() -> 18 | % open underlying memory-mapped file 19 | {ok, MFile, #{size := X}} = emmap:open("/tmp/simple.bin", 0, 1, [create, fit, write, shared]), 20 | ?assert(X >= 1), 21 | 22 | % init block storage of the fixed block size 23 | ok = emmap:init_block_storage(MFile, 1), 24 | 25 | % storing block of a wrong size is an error 26 | ?assertError(badarg, emmap:store_block(MFile, <<1, 2, 3>>)), 27 | 28 | % write-read single blocks in a loop 29 | Addrs = lists:foldl(fun (N, Acc) -> 30 | Data = integer_to_binary(N), 31 | Addr = emmap:store_block(MFile, Data), 32 | Bytes = emmap:read_block(MFile, Addr), 33 | ?assertMatch(Data, Bytes), 34 | [Addr | Acc] 35 | end, [], lists:seq(0, 9)), 36 | 37 | % read all blocks 38 | L1 = emmap:read_blocks(MFile), 39 | ?assert(is_list(L1)), 40 | 41 | % read one big chunk 42 | {L2, eof} = emmap:read_blocks(MFile, 0, 100), 43 | ?assert(is_list(L2)), 44 | ?assertMatch(L1, L2), 45 | 46 | % read in chunks 47 | L3 = read_chunks(MFile, 3), 48 | ?assertMatch(L1, L3), 49 | 50 | Addrs_ = [Addr || {Addr, _Data} <- L1], 51 | ?assertMatch(Addrs_, Addrs), 52 | 53 | % remove all blocks 54 | lists:foreach(fun (Addr) -> 55 | ?assertMatch(true, emmap:free_block(MFile, Addr)) 56 | end, Addrs), 57 | 58 | % ensure stogare is empty now 59 | ?assertMatch([], emmap:read_blocks(MFile)), 60 | 61 | ok. 62 | 63 | sustainability_test() -> 64 | FName = "/tmp/garbage.bin", 65 | Size = 64 * 64 * 64 * 32, 66 | BS = 22, Limo = Size, 67 | 68 | lists:foreach(fun (_) -> 69 | Data = <>, 70 | ok = file:write_file(FName, Data), 71 | {ok, MFile, #{size := Size}} = emmap:open(FName, [write]), 72 | try 73 | % read the whole storage 74 | L1 = emmap:read_blocks(MFile), 75 | ?assert(is_list(L1)), 76 | 77 | % read in chunks 78 | L2 = read_chunks(MFile, 1_000), 79 | ?assert(is_list(L2)), 80 | 81 | % results must be equal 82 | ?assertEqual(L1, L2), 83 | 84 | % repair storage in chunks 85 | ok = repair_chunks(MFile, 10_000), 86 | 87 | % save binary result and list of items 88 | {ok, Bin1} = emmap:pread(MFile, 0, Size), 89 | L3 = read_chunks(MFile, 1_000), 90 | ?assert(is_list(L3)), 91 | 92 | % restore raw data 93 | ok = emmap:pwrite(MFile, 0, Data), 94 | 95 | % repair whole storage 96 | ok = emmap:repair_block_storage(MFile), 97 | 98 | % save binary result and list of items 99 | {ok, Bin2} = emmap:pread(MFile, 0, Size), 100 | L4 = read_chunks(MFile, 1_000), 101 | ?assert(is_list(L4)), 102 | 103 | ?assertMatch(Bin1, Bin2), 104 | ?assertEqual(L3, L4), 105 | 106 | ?debugFmt("~p ~p", [length(L1), length(L3)]), 107 | ?assert(length(L1) =< length(L3)) 108 | catch error:badarg -> 109 | ok 110 | end 111 | end, lists:seq(1, 20)). 112 | 113 | big_random_test_t() -> 114 | FileName = "/tmp/bigrandom.bin", 115 | BlockSize = 1531, 116 | Iterations = 100_000, 117 | MaxSize = 200, 118 | 119 | {ok, MFile, #{size := N}} = emmap:open(FileName, 0, 1, [create, fit, write, shared]), 120 | ?assert(N >= 1), 121 | ok = emmap:init_block_storage(MFile, BlockSize), 122 | 123 | Acc = loop(Iterations, MFile, #{}, fun 124 | (data) -> 125 | rand:bytes(BlockSize); 126 | (Map) -> 127 | map_size(Map) < rand:uniform(MaxSize) 128 | end), 129 | 130 | ?assertMatch(ok, emmap:close(MFile)), 131 | 132 | {ok, MFile_, _} = emmap:open(FileName, [write, shared]), 133 | 134 | loop(Iterations, MFile_, Acc, fun 135 | (data) -> 136 | rand:bytes(BlockSize); 137 | (Map) -> 138 | map_size(Map) < rand:uniform(MaxSize) 139 | end), 140 | 141 | ok. 142 | 143 | loop(0, _, Map, _) -> 144 | Map; 145 | loop(N, MFile, Map, Fun) -> 146 | % ?debugFmt("~p items", [map_size(Map)]), 147 | case Fun(Map) of 148 | true -> 149 | Data = Fun(data), 150 | Addr = emmap:store_block(MFile, Data), 151 | loop(N - 1, MFile, Map#{Addr => Data}, Fun); 152 | false -> 153 | Addr = lists:nth(rand:uniform(map_size(Map)), maps:keys(Map)), 154 | {Data, Map_} = maps:take(Addr, Map), 155 | Bytes = emmap:read_block(MFile, Addr), 156 | ?assertMatch(Data, Bytes), 157 | ?assertMatch(true, emmap:free_block(MFile, Addr)), 158 | loop(N - 1, MFile, Map_, Fun) 159 | end. 160 | 161 | read_chunks(MFile, N) -> 162 | read_chunks(MFile, 0, N, []). 163 | 164 | read_chunks(_MFile, eof, _, Acc) -> 165 | lists:concat(Acc); 166 | read_chunks(MFile, Start, N, Acc) -> 167 | {_Time, {L, Cont}} = timer:tc(emmap, read_blocks, [MFile, Start, N]), 168 | ?assert(is_list(L)), 169 | % ?debugFmt("~p-chunk read in ~p us~n", [N, Time]), 170 | read_chunks(MFile, Cont, N, [L | Acc]). 171 | 172 | repair_chunks(MFile, N) -> 173 | repair_chunks(MFile, 0, N). 174 | 175 | repair_chunks(_MFile, eof, _) -> 176 | ok; 177 | repair_chunks(MFile, Start, N) -> 178 | Cont = emmap:repair_block_storage(MFile, Start, N), 179 | {_Time, Cont} = timer:tc(emmap, repair_block_storage, [MFile, Start, N]), 180 | % ?debugFmt("chunk checked in ~p us~n", [Time]), 181 | repair_chunks(MFile, Cont, N). 182 | 183 | block_storage_test_t() -> 184 | {ok, MFile, #{size := N}} = emmap:open("/tmp/storage.bin", 0, 8, [create, fit, write, shared]), 185 | ?assert(N >= 8), 186 | ok = emmap:init_block_storage(MFile, 8), 187 | write_n_blocks(4096, MFile, 8), 188 | 189 | {T1, L1} = timer:tc(fun () -> emmap:read_blocks(MFile) end), 190 | ?debugFmt("elapsed ~p us~n", [T1]), 191 | ?assert(is_list(L1)), 192 | ?assertMatch(4096, length(L1)), 193 | % ?debugFmt("result: ~p~n", [L1]), 194 | 195 | {T3, L3} = timer:tc(fun () -> read_chunks(MFile, 100) end), 196 | ?debugFmt("elapsed ~p us~n", [T3]), 197 | ?assert(is_list(L3)), 198 | ?assertMatch(4096, length(L3)), 199 | 200 | ?assertMatch(L1, L3), 201 | lists:foreach(fun ({Addr, _Data}) -> 202 | ?assertMatch(true, emmap:free_block(MFile, Addr)) 203 | end, L1), 204 | 205 | ?assertMatch([], emmap:read_blocks(MFile)), 206 | ok. 207 | 208 | write_n_blocks(0, _, _) -> ok; 209 | write_n_blocks(N, MFile, Size) -> 210 | Bytes1 = rand:bytes(Size), 211 | 212 | Addr = emmap:store_block(MFile, Bytes1), 213 | % io:format(user, "addr: ~p~n", [Addr]), 214 | ?assert(is_integer(Addr) andalso Addr >= 0), 215 | 216 | Bytes2 = emmap:read_block(MFile, Addr), 217 | ?assertMatch(Bytes1, Bytes2), 218 | 219 | write_n_blocks(N - 1, MFile, Size). 220 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erlang MMAP `emmap` 2 | 3 | [![build](https://github.com/saleyn/emmap/actions/workflows/erlang.yml/badge.svg)](https://github.com/saleyn/emmap/actions/workflows/erlang.yml) 4 | 5 | This Erlang library implements an ability to use memory map files in the memory of the 6 | Erlang virtual machine. It offers three sets of functions to implement: 7 | 8 | 1. Generic read/write access to memory mapped files. 9 | 2. Persistent atomic integer counters supporting basic arithmetic and logical operators. 10 | This feature is an enhancement of the 11 | [Erlang's atomic counters](https://www.erlang.org/doc/man/counters.html), by adding more 12 | atomic operations (e.g. `xchg`, `cas`, `and`, `or`, and `xor`) for the counters, as well as 13 | adding counter persistence. 14 | 3. Persistent FIFO queue. 15 | 4. Persistent storage for fixed-size data blocks. 16 | 17 | ## Authors 18 | 19 | * [Kresten Krab Thorup](https://github.com/krestenkrab/emmap) 20 | * [Serge Aleynikov](https://github.com/saleyn/emmap) 21 | 22 | ## Supported Platforms 23 | 24 | Linux, MacOS 25 | 26 | NOTE: On MacOS `emmap:resize/2` is not supported, it will return `{error, fixed_size}`. 27 | 28 | ## Basic Usage 29 | 30 | The basic usage is 31 | ```erlang 32 | {ok, Mem, _Info} = emmap:open("filename", [read, shared, direct]), 33 | {ok, Binary} = file:pread(Mem, 100, 40), 34 | ... 35 | ok = file:close(Mem). 36 | ``` 37 | The open options is a list containing zero or more [options](https://saleyn.github.io/emmap/emmap.html#type-open_option). 38 | 39 | From this point, `Mem` can be used either with the `file` or with the `emmap` functions 40 | interchangeably: 41 | 42 | - `{ok, Binary} = file:pread(Mem, Position, Length)` read Length bytes at Position in the file. 43 | - `ok = file:pwrite(Mem, Position, Binary)` writes to the given position. 44 | - `{ok, Binary} = file:read(Mem, Length)` read 1..Length bytes from current position, or return 45 | `eof` if pointer is at end of file. 46 | - `{ok, Pos} = file:position(Mem, Where)` see file:position/2 documentation. 47 | - `ok = file:close(Mem)` 48 | 49 | All read/write functions invoke NIFs that don't call any IO functions but rather access memory 50 | via calls to `memcpy(2)`, and persistence is achieved by relying on the OS implementation of 51 | saving dirty memory pages to files. 52 | 53 | A memory map can be closed either by calling `emmap:close/1` or `file:close/1`. When using 54 | the `direct` option, and `emmap:close/1` is called, the memory map is not immediately closed, 55 | but will get automatically closed when all binaries that reference this memory map are garbage 56 | collected. 57 | 58 | ## Atomic operations on the memory mapped file 59 | 60 | The `emmap` application offers a way to do atomic `add`, `sub`, `xchg`, `cas` as well as bitwise 61 | `and`, `or`, `xor` operations using `emmap:patomic_*/3` and `emmap:patomic_cas/4` functions. 62 | 63 | Effectively this directly changes the content of the underlying memory, is thread-safe, and 64 | persistent. 65 | 66 | ```erlang 67 | {ok, OldValue} = emmap:patomic_add(Mem, Position, 1). 68 | ``` 69 | This approach allows to implement persistent atomic counters that survive node restarts. 70 | 71 | ## Atomic persistent counters 72 | 73 | The `emmap` application allows a user to maintain atomic persistent counters. This could be 74 | useful for continuous numbering of some events in the system which could be efficiently shared 75 | among Erlang or OS processes in a thread-safe way and at the same time being persistent. 76 | This is a very light-weight approach compared to using `mnesia` or other form of persistent 77 | storage. 78 | 79 | Here is an example: 80 | 81 | ```erlang 82 | F = emmap:open_counters("/tmp/mem.bin", 2), 83 | N1 = emmap:inc_counter(F, 0), 84 | N2 = emmap:inc_counter(F, 0, 5), 85 | N3 = emmap:inc_counter(F, 0), 86 | N4 = emmap:inc_counter(F, 0, 2), 87 | N5 = emmap:set_counter(F, 0, 15), 88 | N6 = emmap:read_counter(F, 0), 89 | emmap:close_counters(F), 90 | io_format("N1=~w, N2=~w, N3=~w, N4=~w, N5=~w, N6=~w\n", 91 | [N1, N2, N3, N4, N5, N6]). % Prints: N1=0, N2=1, N3=6, N4=7, N5=9, N6=15 92 | ``` 93 | 94 | ## Shared memory and using mutable binaries 95 | 96 | While Erlang goes at length to achieve immutability, sometimes applications might need 97 | to have access to mutable memory. This can be accomplished by using the direct shared 98 | access to the memory mapped file. 99 | 100 | Example: 101 | 102 | ```erlang 103 | shell1> {ok, MM, _Info} = emmap:open("/tmp/mem.data", 0, 8, [create, direct, read, write, shared, nolock]). 104 | shell2> {ok, MM, _Info} = emmap:open("/tmp/mem.data", 0, 8, [create, direct, read, write, shared, nolock]). 105 | 106 | shell1> emmap:pwrite(MM, 0, <<"test1">>). 107 | shell2> {ok, Bin} = emmap:pread(MM, 0, 5). 108 | {ok, <<"test1">>} 109 | 110 | shell1> emmap:pwrite(MM, 0, <<"test2">>). 111 | shell2> Bin. 112 | <<"test2">> 113 | 114 | shell1> emmap:pwrite(MM, 0, <<"test3">>). 115 | shell2> Bin. 116 | <<"test3">> 117 | ``` 118 | Though this may seem odd that a bound `Bin` variable returns a different value when we printed 119 | it in the `shell2` the second time, it is the result of opening memory mapped file using the 120 | `direct` option. In this case the binaries read from memory map point to the actual memory 121 | in that map rather than being copies of that memory. For some applications, such as when 122 | using that memory to store atomic counters, this property can be very valuable. 123 | 124 | Using the option `direct` has the effect that the mmap file is not closed until all references 125 | to binaries coming out of read/pread have been garbage collected. This is a consequence of 126 | that such binaries are referring directly to the mmap'ed memory. 127 | 128 | When passing `auto_unlink` option to `emmap:open/4`, the memory mapped file will be 129 | automatically deleted when it is closed. 130 | 131 | ## Shared memory without using mutable binaries 132 | 133 | This example preserves the immutability of binaries but allows `emmap` to have visibility 134 | of memory changes between Erlang processes and also between OS processes. 135 | 136 | ```erlang 137 | $ erl -pa _build/default/lib/emmap/ebin 138 | eshell#1> {ok, F, _} = emmap:open("/tmp/q.bin", 0, 128, [auto_unlink, shared, create, read, 139 | write]). 140 | 141 | $ erl -pa _build/default/lib/emmap/ebin 142 | eshell#2> {ok, F, _} = emmap:open("/tmp/q.bin", 0, 128, [auto_unlink, shared, create, read, 143 | write]). 144 | 145 | eshell#1> emmap:pwrite(F, 0, <<"abcdefg\n">>). 146 | 147 | eshell#2> emmap:pread(F, 0, 8). % Changes in eshell#1 are visible in eshell#2 148 | {ok, <<"abcdefg\n">>} 149 | 150 | $ head -1 /tmp/q.bin # They are also visible in another OS process reading from file 151 | abcdefg 152 | ``` 153 | 154 | Here it is without the `shared` option: 155 | ```erlang 156 | $ erl -pa _build/default/lib/emmap/ebin 157 | eshell#1> emmap:close(F). 158 | eshell#1> f(F), {ok, F, _} = emmap:open("/tmp/q.bin", 0, 128, [auto_unlink, create, read, 159 | write]). 160 | 161 | ^G 162 | --> s % Start a new shell process inside the same Erlang VM 163 | --> c 2 % Connect to the new shell 164 | eshell#2> f(F), {ok, F, _} = emmap:open("/tmp/q.bin", 0, 128, [auto_unlink, create, read, 165 | write]). 166 | 167 | ^G 168 | --> c 1 % Switch back to the 1st shell 169 | eshell#1> emmap:pwrite(F, 0, <<"1234567\n">>). 170 | 171 | ^G 172 | --> c 2 % Switch to the 2st shell 173 | 174 | eshell#2> emmap:pread(F, 0, 8). 175 | {ok,<<0,0,0,0,0,0,0,0>>} % changes from shell1 are invisible in the shell2 Erlang process 176 | 177 | # Run this in another terminal 178 | $ head -1 /tmp/q.bin # returns no data because changes in shell1 are invisible 179 | ``` 180 | 181 | ## Persistent FIFO used as a container or guarded by a gen_server process 182 | 183 | The `emmap_queue` module implements a persistent FIFO queue based on a memory-mapped file. 184 | This means that in-memory operations of enqueuing items are automatically persisted on disk. 185 | 186 | A queue used as a container will persistent messages stored in queue on disk, and has constant 187 | time complexity of the push and pop operations. The `open/3` is given an initial storage in 188 | bytes, which will automatically grow unless the `fixed_size` option is provided, in which case 189 | when the queue becomes full, a `push/2` call will return `{error, full}`. In the example below 190 | we are using `auto_unlink` option which automatically deletes the memory mapped file at the end 191 | of the test case (something you might not want in other cases): 192 | 193 | ```erlang 194 | {ok, Q} = emmap_queue:open(Filename, 1024, [auto_unlink]), 195 | ok = emmap_queue:push(Q, a), 196 | ok = emmap_queue:push(Q, {b,1}), 197 | ok = emmap_queue:push(Q, {c,d}), 198 | 199 | a = emmap_queue:pop(Q), 200 | {b,1} = emmap_queue:pop(Q), 201 | {c,d} = emmap_queue:pop(Q), 202 | nil = emmap_queue:pop_and_purge(Q). 203 | ``` 204 | 205 | Use `emmap_queue:pop_and_purge/1` to reclaim the space in memory when the queue becomes empty. 206 | 207 | When a queue is wrapped in a `gen_server`, it is suitable for use in a multi-process use cases. 208 | This is implemented using `emmap_queue:start_link/4`,`emmap_queue:enqueue/2`, and 209 | `emmap_queue:dequeue/1` functions. In the example below we are using the `auto_unlink` option 210 | which automatically deletes the memory mapped file at the end of the test case (something you 211 | might not want in other cases): 212 | 213 | 214 | ```erlang 215 | {ok, Pid} = emmap_queue:start_link(?MODULE, Filename, 1024, [auto_unlink]), 216 | ok = emmap_queue:enqueue(Pid, a), 217 | ok = emmap_queue:enqueue(Pid, {b,1}), 218 | ok = emmap_queue:enqueue(Pid, {c,d}), 219 | 220 | a = emmap_queue:dequeue(Pid), 221 | {b,1} = emmap_queue:dequeue(Pid), 222 | {c,d} = emmap_queue:dequeue(Pid), 223 | nil = emmap_queue:dequeue(Pid). 224 | ``` 225 | 226 | ## Persistent storage for fixed-size data blocks 227 | 228 | The purpose is to store, read, and remove arbitrary data blocks of a fixed size. Each block of 229 | data stored has a unique integer address (internally translated into an offset from the beginning 230 | of the memory-mapped file). Persistent storage tries to reuse a free block closest to the file start 231 | or allocate a new block when needed. The file may be automatically resized (expanded) when required. 232 | 233 | To start using a memory-mapped file as a storage call `emmap:init_block_storage/2` providing emmap 234 | handler and block size (it will be saved in the storage header). 235 | 236 | The new flag `fit` added to emmap:open/4 option list. When set and the existing file opened has a size 237 | less than requested region length, the file will be stretched to the given length. If the file size is 238 | greater than requested, with fit flag the mapped region will fit the file size. Without the `fit` flag 239 | attempt to map existing file of a different size will result in error. 240 | 241 | ```erlang 242 | % open underlying memory-mapped file 243 | {ok, MFile, Info} = emmap:open("storage.bin", 0, 4096, [create, write, fit, shared]), 244 | 245 | % init block storage of the fixed block size 246 | ok = emmap:init_block_storage(MFile, 22), 247 | ``` 248 | 249 | When opening an existing file, it may be possible that it was left in an inconsistent state in 250 | case of abnormal termination of the program modifying it. To ensure consistency, call 251 | `emmap:repair_block_storage/1` to check and fix the file at once, or repeatedly call 252 | `emmap:repair_block_storage/3`. The latter version with continuation is recommended for relatively 253 | big storages, to avoid long-running NIF calls. The repair operation checks (and fixes) 254 | inconsistency between "free blocks" and "used blocks" masks in the internal tree-like representation. 255 | 256 | ```erlang 257 | repair_chunks(MFile, N) -> 258 | repair_chunks(MFile, 0, N). 259 | 260 | repair_chunks(_MFile, eof, _) -> 261 | ok; 262 | repair_chunks(MFile, Start, N) -> 263 | Cont = emmap:repair_block_storage(MFile, Start, N), 264 | repair_chunks(MFile, Cont, N). 265 | ``` 266 | 267 | To read all blocks, use `emmap:read_blocks/1`, or `emmap:read_blocks/3` for reads with continuation, 268 | limiting the number of blocks read in one shot, to avoid long-running NIF calls. 269 | 270 | ```erlang 271 | List = emmap:read_blocks(MFile), 272 | ``` 273 | or 274 | ```erlang 275 | read_chunks(MFile, N) -> 276 | read_chunks(MFile, 0, N, []). 277 | 278 | read_chunks(_MFile, eof, _, Acc) -> 279 | lists:concat(Acc); 280 | read_chunks(MFile, Start, N, Acc) -> 281 | {L, Cont} = emmap:read_blocks(MFile, Start, N), 282 | read_chunks(MFile, Cont, N, [L | Acc]). 283 | ``` 284 | 285 | The function `emmap:read_block/2` reads the block at the given address, `emmap:store_block/2` 286 | writes the data block into the storage, and `emmap:free_block/2` deletes the block at the given 287 | address. 288 | 289 | ```erlang 290 | Addr = emmap:store_block(MFile, Data), 291 | Bytes = emmap:read_block(MFile, Addr), 292 | emmap:free_block(MFile, Addr), 293 | ``` 294 | 295 | The storage capacity is limited by internal organization. It depends on the number of tree levels, and can be 296 | configured by defining the environment variable `BS_LEVELS`. The maximum number of stored blocks is `64 ^ BS_LEVELS`. 297 | The default `BS_LEVELS` value is 3, so the default capacity is `64 * 64 * 64 = 262144` blocks. 298 | 299 | An attempt to store a block will return `{error, full}` when the storage has no free slots. 300 | 301 | The result of freeing a block is `true` on success, `false` if there is no block with the given address 302 | found, or `{error, Reason}` for common emmap error cases. 303 | 304 | The `read_block/2` returns bytes, `eof` when no block exists at the given address or common error. 305 | -------------------------------------------------------------------------------- /c_src/bsna.hpp: -------------------------------------------------------------------------------- 1 | // ex: ts=2 sw=2 ft=cpp et 2 | /** 3 | * \file 4 | * \brief Fixed size blocks storage - non-atomic version 5 | * \author Dmitriy Kargapolov 6 | * \since 30 June 2024 7 | */ 8 | #pragma once 9 | 10 | #ifndef __has_builtin 11 | #error "__has_builtin is not defined" 12 | #endif 13 | 14 | #if not __has_builtin(__builtin_ffsll) 15 | #error "__builtin_ffsll is not defined" 16 | #endif 17 | 18 | #ifndef BS_LEVELS 19 | #define BS_LEVELS 3 20 | #endif 21 | 22 | namespace { 23 | 24 | const uint64_t filled = 0xffff'ffff'ffff'ffff; 25 | const uint64_t vacant = 0x0000'0000'0000'0000; 26 | 27 | static inline uint64_t& free_mask(void* mem) { return ((uint64_t*)mem)[0]; } 28 | static inline uint64_t& used_mask(void* mem) { return ((uint64_t*)mem)[1]; } 29 | 30 | template 31 | inline bool not_used(void* mem) { return used_mask(mem) == vacant; } 32 | template<> 33 | inline bool not_used<1>(void* mem) { return free_mask(mem) == filled; } 34 | 35 | static inline void maybe_init_mask(void* mem, void*& last) { 36 | // initialize ground level mask if not yet done 37 | if (mem > last) { 38 | free_mask(mem) = filled; 39 | last = mem; 40 | } 41 | } 42 | 43 | static inline void maybe_init_masks(void* mem, void*& last) { 44 | // initialize masks if not yet done 45 | if (mem > last) { 46 | free_mask(mem) = filled; 47 | used_mask(mem) = vacant; 48 | last = mem; 49 | } 50 | } 51 | 52 | template inline size_t mask_len() { return 16; } 53 | template<> inline size_t mask_len<1>() { return 8; } 54 | 55 | // calculate block size at given level 56 | // all levels excepting ground carry two 8-byte masks: 57 | // free blocks and allocated blocks masks 58 | template 59 | inline size_t block_size(size_t bs, int n = 64) { 60 | return mask_len() + n * block_size(bs); 61 | } 62 | 63 | // ground level carry onle "available" mask 64 | template<> 65 | inline size_t block_size<1>(size_t bs, int n) { 66 | return mask_len<1>() + n * bs; 67 | } 68 | 69 | // translate address to pointer 70 | template 71 | inline char *ptr(void* mem, int n, size_t bs) { 72 | return (char *)mem + block_size(bs, n); 73 | } 74 | 75 | // translate address to a pointer 76 | template 77 | inline void* pointer(void* mem, int addr, size_t bs) { 78 | return pointer(ptr(mem, addr % 64, bs), addr / 64, bs); 79 | } 80 | 81 | template<> 82 | inline void* pointer<0>(void* mem, int, size_t) { return mem; } 83 | 84 | struct limits { 85 | size_t bs; 86 | void* end; 87 | void* last; 88 | 89 | limits(size_t b, void* e, void* l) : bs(b), end(e), last(l) {} 90 | 91 | template 92 | bool mask_over(const void* ptr) const { 93 | return (char *)ptr + mask_len() > end; 94 | } 95 | 96 | template 97 | bool mask_undef(const void* ptr) const { 98 | return ptr > last || (char *)ptr + mask_len() > end; 99 | } 100 | 101 | bool data_over(const void* ptr) const { 102 | return (char *)ptr + bs > end; 103 | } 104 | }; 105 | 106 | // translate address to pointer to an existing block 107 | template 108 | inline void* real_pointer(void* mem, int addr, const limits& lim) { 109 | if (lim.mask_undef(mem)) return nullptr; 110 | void* p = ptr(mem, addr % 64, lim.bs); 111 | return real_pointer(p, addr / 64, lim); 112 | } 113 | 114 | template<> 115 | inline void* real_pointer<1>(void* mem, int addr, const limits& lim) { 116 | if (lim.mask_undef<1>(mem)) return nullptr; 117 | int n = addr % 64; 118 | // attempt to access unallocated block? 119 | if ((free_mask(mem) & (1ul << n)) != vacant) return nullptr; 120 | void* p = ptr<1>(mem, n, lim.bs); 121 | return lim.data_over(p) ? nullptr : p; 122 | } 123 | 124 | // free block 125 | template 126 | inline bool free_block(void* mem, int addr, const limits& lim) { 127 | // bit 128 | int n = addr % 64; 129 | uint64_t bit = 1ul << n; 130 | bool ret; 131 | 132 | // next level pointer 133 | void* p = ptr(mem, n, lim.bs); 134 | if (lim.mask_undef(p)) { 135 | ret = false; 136 | 137 | // clear presence mask 138 | used_mask(mem) &= ~bit; 139 | } 140 | else { 141 | ret = free_block(p, addr / 64, lim); 142 | 143 | // clear presence mask if next level got empty 144 | if (not_used(p)) used_mask(mem) &= ~bit; 145 | } 146 | 147 | // mark block free 148 | free_mask(mem) |= bit; 149 | 150 | return ret; 151 | } 152 | 153 | // free block 154 | template<> 155 | inline bool free_block<1>(void* mem, int addr, const limits&) { 156 | // bit 157 | int n = addr % 64; 158 | uint64_t bit = 1ul << n; 159 | 160 | // attempt to free unallocated block? 161 | if ((free_mask(mem) & bit) != vacant) return false; 162 | 163 | // mark block free 164 | free_mask(mem) |= bit; 165 | 166 | return true; 167 | } 168 | 169 | template 170 | int alloc(void* mem, limits& lim) { 171 | if (lim.mask_over(mem)) return -2; 172 | maybe_init_masks(mem, lim.last); 173 | uint64_t& free_bits = free_mask(mem); 174 | 175 | while (true) { 176 | // find group that not yet filled (marked by 1) 177 | int n = __builtin_ffsll(free_bits) - 1; 178 | if (n < 0) return -1; 179 | 180 | // pointer to the group mask 181 | char *p = ptr(mem, n, lim.bs); 182 | 183 | // find subgroup or free block 184 | int m = alloc(p, lim); 185 | if (m < -1) return m; 186 | 187 | // n-th bit 188 | uint64_t bit = 1ul << n; 189 | 190 | if (m < 0) { 191 | // ensure free mask bit cleared to mark group filled 192 | free_bits ^= bit; 193 | 194 | // try another group 195 | continue; 196 | } 197 | 198 | // if group is filled, clear free mask bit 199 | if (*(uint64_t *)p == 0) 200 | free_bits ^= bit; 201 | 202 | // ensure used mask bit is set 203 | used_mask(mem) |= bit; 204 | 205 | // return index 206 | return m * 64 + n; 207 | } 208 | } 209 | 210 | template<> 211 | int alloc<1>(void* mem, limits& lim) { 212 | if (lim.mask_over<1>(mem)) return -2; 213 | maybe_init_mask(mem, lim.last); 214 | uint64_t& free_bits = free_mask(mem); 215 | 216 | // find block that not yet filled (marked by 1) 217 | int n = __builtin_ffsll(free_bits) - 1; 218 | if (n < 0) return -1; 219 | 220 | // pointer to the data block 221 | char *p = ptr<1>(mem, n, lim.bs); 222 | if (lim.data_over(p)) return -2; 223 | 224 | // flip 1-bit to 0 marking block allocated 225 | free_bits ^= 1ul << n; 226 | 227 | return n; 228 | } 229 | 230 | template 231 | int store(void* mem, const ErlNifBinary& bin, limits& lim) { 232 | int n = alloc(mem, lim); 233 | if (n < 0) return n; 234 | memcpy(pointer(mem, n, lim.bs), bin.data, bin.size); 235 | return n; 236 | } 237 | 238 | // fold through stored blocks 239 | 240 | template 241 | static inline constexpr const int c_flag() { return 1 << N * 6; } 242 | 243 | template 244 | static inline int mkaddr(int n, int base) { return base + (n << (BS_LEVELS - N) * 6); } 245 | 246 | template struct proc { 247 | 248 | template 249 | static void fold(int base, void* mem, const limits& lim, F fun) { 250 | uint64_t bit = 1ul; 251 | for (int n = 0; n < 64; ++n, bit <<= 1) { 252 | if ((used_mask(mem) & bit) != bit) 253 | continue; 254 | void* p = ptr(mem, n, lim.bs); 255 | if (lim.mask_undef(p)) 256 | break; 257 | proc::fold(mkaddr(n, base), p, lim, fun); 258 | } 259 | } 260 | 261 | template 262 | static int fold(int base, void* mem, const limits& lim, int addr, F fun) { 263 | int start = addr % 64; 264 | uint64_t bit = 1ul << start; 265 | for (int n = start; n < 64; ++n, bit <<= 1) { 266 | if ((used_mask(mem) & bit) != bit) 267 | continue; 268 | void* p = ptr(mem, n, lim.bs); 269 | if (lim.mask_undef(p)) 270 | break; 271 | int next = n == start ? addr / 64 : 0; 272 | int ret = proc::fold(mkaddr(n, base), p, lim, next, fun); 273 | if (ret < 0) 274 | return ret; 275 | if (ret == 0) 276 | continue; 277 | // hit the limit, return continuation part 278 | int cflag = c_flag(); 279 | if ((cflag & ret) == cflag) 280 | if (n == 63) 281 | return c_flag() | ((ret ^ cflag) * 64); 282 | else 283 | return (ret ^ cflag) * 64 + n + 1; 284 | else 285 | return ret * 64 + n; 286 | } 287 | // continue 288 | return 0; 289 | } 290 | 291 | static std::tuple repair(void* mem, const limits& lim) { 292 | uint64_t bit = 1ul; 293 | 294 | for (int n = 0; n < 64; ++n, bit <<= 1) { 295 | bool l_used = used_mask(mem) & bit; 296 | bool l_free = free_mask(mem) & bit; 297 | void* p = ptr(mem, n, lim.bs); 298 | 299 | if (lim.mask_undef(p)) { 300 | if (l_used) used_mask(mem) &= ~bit; 301 | if (!l_free) free_mask(mem) |= bit; 302 | continue; 303 | } 304 | 305 | auto [r_used, r_free] = proc::repair(p, lim); 306 | if (r_used) { 307 | if (!l_used) used_mask(mem) |= bit; 308 | } 309 | else { 310 | if (l_used) used_mask(mem) &= ~bit; 311 | } 312 | if (r_free) { 313 | if (!l_free) free_mask(mem) |= bit; 314 | } 315 | else { 316 | if (l_free) free_mask(mem) &= ~bit; 317 | } 318 | } 319 | 320 | return std::tuple(used_mask(mem) != vacant, free_mask(mem) != vacant); 321 | } 322 | 323 | static std::tuple repair(void* mem, const limits& lim, int addr, int& left) { 324 | int start = addr % 64; 325 | uint64_t bit = 1ul << start; 326 | for (int n = start; n < 64; ++n, bit <<= 1) { 327 | bool l_used = used_mask(mem) & bit; 328 | bool l_free = free_mask(mem) & bit; 329 | void* p = ptr(mem, n, lim.bs); 330 | 331 | if (lim.mask_undef(p)) { 332 | if (l_used) used_mask(mem) &= ~bit; 333 | if (!l_free) free_mask(mem) |= bit; 334 | continue; 335 | } 336 | 337 | int next = n == start ? addr / 64 : 0; 338 | auto [ret, r_used, r_free] = proc::repair(p, lim, next, left); 339 | 340 | // repair used mask 341 | if (r_used) { 342 | if (!l_used) used_mask(mem) |= bit; 343 | } 344 | else { 345 | if (l_used) used_mask(mem) &= ~bit; 346 | } 347 | 348 | // repair free mask 349 | if (r_free) { 350 | if (!l_free) free_mask(mem) |= bit; 351 | } 352 | else { 353 | if (l_free) free_mask(mem) &= ~bit; 354 | } 355 | 356 | // hit the limit? 357 | if (ret > 0) { 358 | int cflag = c_flag(); 359 | if ((cflag & ret) == cflag) 360 | if (n == 63) 361 | ret = c_flag() | ((ret ^ cflag) * 64); 362 | else 363 | ret = (ret ^ cflag) * 64 + n + 1; 364 | else 365 | ret = ret * 64 + n; 366 | if (n == 63) 367 | return std::tuple(ret, used_mask(mem) != vacant, free_mask(mem) != vacant); 368 | else 369 | return std::tuple(ret, false, false); 370 | } 371 | } 372 | 373 | return std::tuple(0, used_mask(mem) != vacant, free_mask(mem) != vacant); 374 | } 375 | 376 | }; 377 | 378 | template<> struct proc<1> { 379 | 380 | template 381 | static void fold(int base, void* mem, const limits& lim, F fun) { 382 | uint64_t bit = 1ul; 383 | for (int n = 0; n < 64; ++n, bit <<= 1) { 384 | if ((free_mask(mem) & bit) == bit) 385 | continue; 386 | void* p = ptr<1>(mem, n, lim.bs); 387 | if (lim.data_over(p)) 388 | break; 389 | fun(mkaddr<1>(n, base), p, lim.bs); 390 | } 391 | } 392 | 393 | template 394 | static int fold(int base, void* mem, const limits& lim, int addr, F fun) { 395 | int start = addr % 64; 396 | uint64_t bit = 1ul << start; 397 | for (int n = start; n < 64; ++n, bit <<= 1) { 398 | if ((free_mask(mem) & bit) == bit) 399 | continue; 400 | void* p = ptr<1>(mem, n, lim.bs); 401 | if (lim.data_over(p)) 402 | return -1; 403 | if (!fun(mkaddr<1>(n, base), p, lim.bs)) { 404 | // hit the limit 405 | if (n == 63) 406 | return c_flag<1>(); 407 | else 408 | return n + 1; 409 | } 410 | } 411 | return 0; 412 | } 413 | 414 | static std::tuple repair(void* mem, const limits& lim) { 415 | uint64_t bit = 1ul; 416 | for (int n = 0; n < 64; ++n, bit <<= 1) { 417 | bool l_free = free_mask(mem) & bit; 418 | void* p = ptr<1>(mem, n, lim.bs); 419 | if (lim.data_over(p)) { 420 | if (!l_free) free_mask(mem) |= bit; 421 | } 422 | } 423 | return std::tuple(free_mask(mem) != filled, free_mask(mem) != vacant); 424 | } 425 | 426 | static std::tuple repair(void* mem, const limits& lim, int addr, int& left) { 427 | int start = addr % 64; 428 | uint64_t bit = 1ul << start; 429 | 430 | for (int n = start; n < 64; ++n, bit <<= 1) { 431 | bool l_free = free_mask(mem) & bit; 432 | void* p = ptr<1>(mem, n, lim.bs); 433 | 434 | if (lim.data_over(p)) { 435 | if (!l_free) free_mask(mem) |= bit; 436 | } 437 | 438 | // hit the limit? 439 | if (--left == 0) { 440 | if (n == 63) 441 | return std::tuple(c_flag<1>(), free_mask(mem) != filled, free_mask(mem) != vacant); 442 | else 443 | return std::tuple(n + 1, false, false); 444 | } 445 | } 446 | 447 | return std::tuple(0, free_mask(mem) != filled, free_mask(mem) != vacant); 448 | } 449 | 450 | }; 451 | 452 | } 453 | 454 | struct bs_head { 455 | uint32_t block_size; 456 | ssize_t limo; // Last Initialized Mask Offset 457 | 458 | void init(unsigned block_size_) { 459 | block_size = block_size_; 460 | limo = -1; 461 | } 462 | 463 | int store(void* mem, void* end, const ErlNifBinary& bin) { 464 | limits lim(block_size, end, (char *)mem + limo); 465 | int ret = ::store(mem, bin, lim); 466 | limo = (char *)lim.last - (char *)mem; 467 | return ret; 468 | } 469 | 470 | template 471 | bool read(void* mem, void* end, int addr, T consume) { 472 | limits lim(block_size, end, (char *)mem + limo); 473 | void* ptr = ::real_pointer(mem, addr, lim); 474 | if (ptr) consume(ptr, block_size); 475 | return ptr; 476 | } 477 | 478 | bool free(void* mem, void* end, int addr) { 479 | limits lim(block_size, end, (char *)mem + limo); 480 | if (lim.mask_undef(mem)) return false; 481 | return ::free_block(mem, addr, lim); 482 | } 483 | 484 | template 485 | void fold(void* mem, void* end, F fun) { 486 | limits lim(block_size, end, (char *)mem + limo); 487 | if (lim.mask_undef(mem)) return; 488 | proc::fold(0, mem, lim, fun); 489 | } 490 | 491 | template 492 | int fold(void* mem, void* end, int start, F fun) { 493 | limits lim(block_size, end, (char *)mem + limo); 494 | if (lim.mask_undef(mem)) return -1; 495 | int ret = proc::fold(0, mem, lim, start, fun); 496 | if (ret > 0 && (c_flag() & ret) > 0) ret = 0; 497 | return ret; 498 | } 499 | 500 | void repair(void* mem, void* end) { 501 | limits lim(block_size, end, (char *)mem + limo); 502 | if (lim.mask_undef(mem)) return; 503 | proc::repair(mem, lim); 504 | } 505 | 506 | int repair(void* mem, void* end, int start, int max) { 507 | limits lim(block_size, end, (char *)mem + limo); 508 | if (lim.mask_undef(mem)) return 0; 509 | return std::get<0>(proc::repair(mem, lim, start, max)); 510 | } 511 | }; 512 | -------------------------------------------------------------------------------- /src/emmap_queue.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @doc Persistent FIFO queue 3 | %%% The FIFO queue can be used as a persistent container of messages 4 | %%% with constant time push and pop operations. Additionally, this 5 | %%% module provides a gen_server API, which wraps the queue for use 6 | %%% in multi-process applications. 7 | %%% The queue is stored in a memory-mapped file, and it automatically 8 | %%% grows if the messages are not dequeued from the queue. Messages 9 | %%% stored in the queue can be compressed using variable compression 10 | %%% level controlled by the argument to the `push/3' function. 11 | %%% See test cases at the end of this module for sample use cases. 12 | %%% @author Serge Aleynikov 13 | %%% @end 14 | %%%----------------------------------------------------------------------------- 15 | %%% Created: 2021-12-10 16 | %%%----------------------------------------------------------------------------- 17 | -module(emmap_queue). 18 | -export([open/3, close/1, flush/1]). 19 | -export([purge/1, is_empty/1, length/1, metadata/1, push/2, push/3, 20 | pop/1, pop/3, peek_front/1, peek_back/1, peek/3, rpeek/3, try_pop/2, 21 | pop_and_purge/1, try_pop_and_purge/2, erase/1]). 22 | 23 | -export([start_link/4, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). 24 | -export([enqueue/2, dequeue/1, try_dequeue/2, inspect/1, info/1]). 25 | 26 | -export_type([queue/0]). 27 | 28 | -compile({no_auto_import,[length/1, erase/1]}). 29 | 30 | -behavior(gen_server). 31 | 32 | -define(HEADER_SZ, 16). 33 | -define(INIT_SEGM_HEADER, 34 | <>). 35 | -define(INIT_SEGM_HEADER_WITH_SIZE(Size), 36 | <>). 37 | 38 | -ifdef(TEST). 39 | -include_lib("eunit/include/eunit.hrl"). 40 | -endif. 41 | 42 | -include_lib("kernel/include/file.hrl"). 43 | 44 | -type queue() :: emmap:mmap_file(). 45 | 46 | -record(state, { 47 | filename :: string() 48 | , mem :: queue() 49 | , segm_sz :: non_neg_integer() 50 | }). 51 | 52 | -define(TIMEOUT, infinity). 53 | 54 | %%------------------------------------------------------------------------------ 55 | %% Gen_server API 56 | %%------------------------------------------------------------------------------ 57 | 58 | %% @doc Start a queue process. 59 | %% `Name' - name of the registered process. 60 | %% `Filename' - name of the memory mapped file. 61 | %% `SegmSize' - size of the memory mapped segment (can be 0 for an existing file). 62 | %% `Opts' - see `emmap:open_options()'. 63 | start_link(Name, Filename, SegmSize, Opts) when is_list(Opts) -> 64 | gen_server:start_link({local, Name}, ?MODULE, [Filename, SegmSize, Opts], []). 65 | 66 | %% @doc Enqueue a message to the queue 67 | enqueue(Name, Term) -> 68 | gen_server:call(Name, {push, Term}, ?TIMEOUT). 69 | 70 | %% @doc Dequeue a message from the queue 71 | dequeue(Name) -> 72 | gen_server:call(Name, pop, ?TIMEOUT). 73 | 74 | %% @doc Dequeue a message from the queue. 75 | %% The function returns `Res' where `Res' is the result of evaluating 76 | %% the `Fun' with the poped element from the queue. If the `Fun' throws an 77 | %% exception, the exception if propagated to the caller. 78 | try_dequeue(Name, Fun) when is_function(Fun, 1) -> 79 | case gen_server:call(Name, {try_pop, Fun}, ?TIMEOUT) of 80 | {ok, Res} -> 81 | Res; 82 | {error, Error, Stacktrace} -> 83 | erlang:error(Error, Stacktrace) 84 | end. 85 | 86 | %% @doc Peek a message from the queue without dequeuing it 87 | inspect(Name) -> 88 | gen_server:call(Name, peek, ?TIMEOUT). 89 | 90 | %% @doc Return queue info as a map 91 | -spec info(atom()) -> map(). 92 | info(Name) -> 93 | gen_server:call(Name, info, ?TIMEOUT). 94 | 95 | %%------------------------------------------------------------------------------ 96 | %% Gen_server callbacks 97 | %%------------------------------------------------------------------------------ 98 | 99 | init([Filename, SegmSize, Opts]) -> 100 | erlang:process_flag(trap_exit, true), 101 | {ok, Mem} = open(Filename, SegmSize, Opts), 102 | {ok, #state{mem=Mem, filename=Filename, segm_sz=SegmSize}}. 103 | 104 | handle_call({push, Term}, _From, #state{mem=Mem} = State) -> 105 | case push(Mem, Term) of 106 | ok -> 107 | {reply, ok, State}; 108 | {error, Reason} -> 109 | {reply, {error, Reason}, State} 110 | end; 111 | 112 | handle_call(pop, _From, #state{mem=Mem} = State) -> 113 | Msg = pop_and_purge(Mem), 114 | {reply, Msg, State}; 115 | 116 | handle_call(peek, _From, #state{mem=Mem} = State) -> 117 | Msg = peek_front(Mem), 118 | {reply, Msg, State}; 119 | 120 | handle_call({try_pop, Fun}, _From, #state{mem=Mem} = State) -> 121 | try 122 | {reply, {ok, try_pop_and_purge(Mem, Fun)}, State} 123 | catch _:E:ST -> 124 | {reply, {error, E, ST}, State} 125 | end; 126 | 127 | handle_call(info, _From, #state{mem=Mem} = State) -> 128 | {reply, metadata(Mem), State}. 129 | 130 | handle_cast(Msg, State) -> 131 | {stop, {cast_not_implemented, Msg}, State}. 132 | 133 | handle_info({'EXIT', _Pid, Reason}, State) -> 134 | {stop, Reason, State}. 135 | 136 | terminate(_Reason, #state{mem=Mem}) -> 137 | close(Mem). 138 | 139 | %%------------------------------------------------------------------------------ 140 | %% Raw segment access functions 141 | %%------------------------------------------------------------------------------ 142 | 143 | %% @doc Open a memory mapped queue. 144 | -spec open(binary()|list(), integer(), list()) -> 145 | {ok, queue()} | {error, term()}. 146 | open(Filename, Size, Opts) when is_integer(Size), is_list(Opts) -> 147 | ok = filelib:ensure_dir(filename:dirname(Filename)), 148 | case emmap:open(Filename, 0, Size, [create, read, write | Opts]) of 149 | {ok, Mem, #{exist := true, size := SegmSize}} -> 150 | case header(Mem) of 151 | {_Head, Tail, Tail, SSize} when SSize =/= SegmSize -> 152 | %% Repair size 153 | update_size(Mem, SegmSize), 154 | {ok, Mem}; 155 | {_Head, Tail, Tail, _SegmSize} -> 156 | {ok, Mem}; 157 | {_Head, Tail, _NextTail, _SegmSize} -> 158 | % Need to repair last uncommitted tail 159 | reserve_tail(Mem, Tail), 160 | {ok, Mem} 161 | end; 162 | {ok, Mem, #{exist := false, size := SegmSize}} -> 163 | ok = emmap:pwrite(Mem, 0, ?INIT_SEGM_HEADER_WITH_SIZE(SegmSize)), 164 | {ok, Mem}; 165 | Error -> 166 | Error 167 | end. 168 | 169 | %% @doc Close a previously open queue. 170 | -spec close(queue()) -> ok. 171 | close(Mem) -> 172 | emmap:close(Mem). 173 | 174 | %% @doc Asynchronously flush the modified memory used by the queue to disk. 175 | %% See notes of `emmap:sync/1'. This call is optional. 176 | -spec flush(queue()) -> ok. 177 | flush(Mem) -> 178 | emmap:flush(Mem). 179 | 180 | %% @doc Purge queue. It is a constant time operation. 181 | -spec purge(queue()) -> boolean(). 182 | purge(Mem) -> 183 | case is_empty_queue(Mem) of 184 | {true, 0} -> 185 | true; 186 | {true, _} -> 187 | init_header(Mem), 188 | true; 189 | false -> 190 | false 191 | end. 192 | 193 | %% @doc Get queue metadata 194 | -spec metadata(queue()) -> 195 | #{head => integer(), tail => integer(), next_tail => integer(), size => integer()}. 196 | metadata(Mem) -> 197 | {Head, Tail, NextTail, SegmSize} = header(Mem), 198 | #{head => Head, tail => Tail, next_tail => NextTail, size => SegmSize}. 199 | 200 | header(Mem) -> 201 | case emmap:pread(Mem, 0, 16) of 202 | {ok, <>} 203 | when ?HEADER_SZ =< H, H =< T, T =< NextT, NextT =< Size -> 204 | {H, T, NextT, Size}; 205 | {ok, Hdr} -> 206 | erlang:error({invalid_queue_header, Hdr}); 207 | {error, Err} -> 208 | erlang:error({cannot_read_header, Err}) 209 | end. 210 | 211 | %% @doc Returns `true' if the queue is empty. 212 | is_empty(Mem) -> 213 | case is_empty_queue(Mem) of 214 | {true, _Head} -> true; 215 | false -> false 216 | end. 217 | 218 | is_empty_queue(#file_descriptor{} = Mem) -> 219 | case emmap:pread(Mem, 0, 16) of 220 | {ok, Header} -> 221 | is_empty_queue(Header); 222 | {error, Error} -> 223 | erlang:error(Error) 224 | end; 225 | is_empty_queue(<<_Sz:4/binary, H:32/integer, H:32/integer, _/binary>>) -> 226 | {true, H}; 227 | is_empty_queue(_Header) -> 228 | false. 229 | 230 | %% @doc Get queue length. This function has a linear-time complexity. 231 | length(Mem) -> 232 | F = fun 233 | G(H, T, Count) when H < T -> 234 | {ok, <>} = emmap:pread(Mem, H, 4), 235 | G(H + Sz, T, Count+1); 236 | G(_, _, Count) -> 237 | Count 238 | end, 239 | {Head, Tail, _, _} = header(Mem), 240 | F(Head, Tail, 0). 241 | 242 | %% @doc Push a term to the queue. This function has a constant-time complexity. 243 | push(Mem, Term) -> 244 | push(Mem, Term, 0). 245 | 246 | %% @doc Push a term to the queue. This function has a constant-time complexity. 247 | %% `Compression' is the compression level from `0' to `9', where `0' is no compression. 248 | push(Mem, Term, Compression) when is_integer(Compression), Compression >= 0, Compression < 10 -> 249 | Bin = term_to_binary(Term, [{compressed, Compression}, {minor_version, 2}]), 250 | Sz0 = byte_size(Bin)+8, 251 | Pad = Sz0 rem 8, 252 | Sz = Sz0 + Pad, 253 | Msg = <>, 254 | Arg = header(Mem), 255 | (fun 256 | G({_Head,_Tail, NextTail, SegmSize}) when NextTail+Sz > SegmSize -> 257 | case emmap:resize(Mem) of 258 | {ok, NewSegmSize} -> 259 | update_size(Mem, NewSegmSize), 260 | G(header(Mem)); 261 | {error, fixed_size} -> 262 | {error, full}; % Queue is full and was given fixed_size option when opened 263 | {error, Reason} -> 264 | {error, Reason} 265 | end; 266 | G({_Head, Tail, NextTail,_SegmSize}) -> 267 | NewTail = NextTail+Sz, 268 | TailBin = <>, 269 | reserve_tail(Mem, TailBin), % Reserve the next tail offset 270 | ok = emmap:pwrite(Mem, Tail, Msg), % Write message 271 | commit_tail(Mem, TailBin) % Commit tail offset 272 | end)(Arg). 273 | 274 | %% @doc Pop a term from the queue. This function has a constant-time complexity. 275 | pop(Mem) -> 276 | case read_head(Mem, true) of 277 | {Msg, _IsEnd} -> 278 | Msg; 279 | nil -> 280 | nil 281 | end. 282 | 283 | %% @doc Evaluate the `Fun' function on the next term in the queue. 284 | %% If the function doesn't raise an exception, pop the term from the queue, otherwise 285 | %% leave the queue unmodified. This function has a constant-time complexity. It returns 286 | %% the result of evaluating `Fun'. 287 | try_pop(Mem, Fun) when is_function(Fun, 1) -> 288 | case read_head(Mem, Fun) of 289 | {Res, _IsEnd} -> 290 | Res; 291 | nil -> 292 | nil 293 | end. 294 | 295 | %% @doc Evaluate the `Fun' function on the next term in the queue. 296 | %% If the function doesn't raise an exception, pop the term from the queue, otherwise 297 | %% leave the queue unmodified. This function has a constant-time complexity. It returns 298 | %% the result of evaluating `Fun'. 299 | try_pop_and_purge(Mem, Fun) when is_function(Fun, 1) -> 300 | case read_head(Mem, Fun) of 301 | {Res, true} -> 302 | init_header(Mem), 303 | Res; 304 | {Res, false} -> 305 | Res; 306 | nil -> 307 | nil 308 | end. 309 | 310 | %% @doc Peek the next awaiting term from the head of the FIFO queue without popping it. 311 | %% This function has a constant-time complexity. 312 | peek_front(Mem) -> 313 | case read_head(Mem, false) of 314 | {Msg, _IsEnd} -> 315 | Msg; 316 | nil -> 317 | nil 318 | end. 319 | 320 | %% @doc Peek the last written term at the back of the FIFO queue without removing it. 321 | %% This function has a constant-time complexity. 322 | peek_back(Mem) -> 323 | {Head, Tail, _NextTail, _Size} = header(Mem), 324 | {Msg, _PrevT} = read_last(Mem, Head, Tail), 325 | Msg. 326 | 327 | %% @doc Pop a term from the queue and reclaim queue's memory if the queue is empty. 328 | %% This function has a constant-time complexity. 329 | pop_and_purge(Mem) -> 330 | case read_head(Mem, true) of 331 | {Msg, true} -> 332 | init_header(Mem), 333 | Msg; 334 | {Msg, false} -> 335 | Msg; 336 | nil -> 337 | nil 338 | end. 339 | 340 | %% @doc Inspect all messages in the queue iteratively by passing them to a custom 341 | %% lambda function. The `Fun' takes a message and state and returns a `{cont, State}' 342 | %% to continue or `{halt, State}' to abort. 343 | peek(Mem, Init, Fun) when is_function(Fun, 2) -> 344 | F = fun 345 | G(H, T, State) when H < T -> 346 | {Msg, NextH} = read_first(Mem, H), 347 | case Fun(Msg, State) of 348 | {cont, State1} -> 349 | G(NextH, T, State1); 350 | {halt, State1} -> 351 | State1 352 | end; 353 | G(_, _, State) -> 354 | State 355 | end, 356 | {Head, Tail, _, _} = header(Mem), 357 | F(Head, Tail, Init). 358 | 359 | %% @doc Inspect all messages in the queue iteratively in the reverse order by passing 360 | %% them to a custom lambda function. The `Fun' takes a message and state and returns a 361 | %% `{cont, State}' to continue or `{halt, State}' to abort. 362 | rpeek(Mem, Init, Fun) when is_function(Fun, 2) -> 363 | F = fun G(H, T, State) -> 364 | case read_last(Mem, H, T) of 365 | {nil, _} -> 366 | State; 367 | {Msg, PrevT} -> 368 | case Fun(Msg, State) of 369 | {cont, State1} -> 370 | G(H, PrevT, State1); 371 | {halt, State1} -> 372 | State1 373 | end 374 | end 375 | end, 376 | {Head, Tail, _, _} = header(Mem), 377 | F(Head, Tail, Init). 378 | 379 | %% @doc Pop messages from the queue by passing them to a custom lambda function. 380 | %% The `Fun' takes a message and state and returns a `{cont, State}' to continue or 381 | %% `{halt, State}' to abort. 382 | pop(Mem, Init, Fun) when is_function(Fun, 2) -> 383 | F = fun 384 | G(H, T, State) when H < T -> 385 | {Msg, NextH} = read_first(Mem, H), 386 | case Fun(Msg, State) of 387 | {cont, State1} -> 388 | update_head(Mem, NextH), 389 | G(NextH, T, State1); 390 | {halt, State1} -> 391 | State1 392 | end; 393 | G(?HEADER_SZ, _, State) -> 394 | State; 395 | G(_, _, State) -> 396 | % Purge the empty queue 397 | init_header(Mem), 398 | State 399 | end, 400 | {Head, Tail, _, _} = header(Mem), 401 | F(Head, Tail, Init). 402 | 403 | %% @doc Erase the content of the queue. All messages in the queue are discarded! 404 | erase(Mem) -> 405 | init_header(Mem). 406 | 407 | read_head(Mem, Pop) -> 408 | case header(Mem) of 409 | {Head, Tail, NextTail, _SegmSize} when Head < Tail -> 410 | {Msg, NextHead} = read_first(Mem, Head), 411 | IsEnd = (NextHead =:= Tail) andalso (NextHead =:= NextTail), 412 | case Pop of 413 | true -> 414 | update_head(Mem, NextHead), 415 | {Msg, IsEnd}; 416 | false -> 417 | {Msg, IsEnd}; 418 | _ when is_function(Pop, 1) -> 419 | Res = Pop(Msg), 420 | update_head(Mem, NextHead), 421 | {Res, IsEnd} 422 | end; 423 | {_Head, _Tail, _NextTail, _} -> 424 | nil 425 | end. 426 | 427 | read_first(Mem, Head) -> 428 | % Read the size of the next message 429 | {ok, <>} = emmap:pread(Mem, Head, 4), 430 | BinSz = Sz-8, % The size read is inclusive of the 2 sizes written before/after the msg 431 | {ok, <>} = emmap:pread(Mem, Head+4, BinSz+4), 432 | EndSz /= Sz andalso erlang:error({message_size_mismatch, {Sz, EndSz}, Head}), 433 | {binary_to_term(Bin), Head+Sz}. 434 | 435 | read_last(Mem, Head, Tail) when Head < Tail-8 -> 436 | % Read the size of the next message 437 | {ok, <>} = emmap:pread(Mem, Tail-4, 4), 438 | case Tail-Sz of 439 | I when I >= Head -> 440 | BinSz = Sz-8, % The size read is inclusive of the 2 sizes written before/after the msg 441 | case emmap:pread(Mem, I, Sz-4) of 442 | {ok, <>} -> 443 | {binary_to_term(Bin), I}; 444 | {error, Why} -> 445 | erlang:error({invalid_msg_header, Why}) 446 | end; 447 | I -> 448 | {nil, I} 449 | end; 450 | read_last(_Mem, I, I) -> 451 | {nil, I}. 452 | 453 | update_head(Mem, NextHead) -> 454 | ok = emmap:pwrite(Mem, 4, <>). 455 | 456 | update_size(Mem, MemSize) -> 457 | ok = emmap:pwrite(Mem, 0, <>). 458 | 459 | reserve_tail(Mem, Tail) when is_integer(Tail) -> 460 | ok = emmap:pwrite(Mem, 12, <>); 461 | reserve_tail(Mem, Tail) when byte_size(Tail) == 4 -> 462 | ok = emmap:pwrite(Mem, 12, Tail). 463 | 464 | commit_tail(Mem, Tail) when byte_size(Tail) == 4 -> 465 | ok = emmap:pwrite(Mem, 8, Tail). 466 | 467 | init_header(Mem) -> 468 | ok = emmap:pwrite(Mem, 4, ?INIT_SEGM_HEADER). 469 | 470 | %%------------------------------------------------------------------------------ 471 | %% Multi-segment gen_server support 472 | %%------------------------------------------------------------------------------ 473 | 474 | -ifdef(EUNIT). 475 | 476 | %% Single-producer-single-consumer 477 | spsc_queue_test() -> 478 | Filename = "/tmp/queue1.bin", 479 | {ok, Mem} = open(Filename, 1024, [auto_unlink]), %% Automatically delete file 480 | ?assert(filelib:is_regular(Filename)), 481 | ?assertEqual(nil, peek_front(Mem)), 482 | ?assertEqual(nil, peek_back(Mem)), 483 | % Enqueue data 484 | ?assertEqual(ok, push(Mem, a)), 485 | ?assertMatch(#{head := 16, tail := 32, next_tail := 32, size := 1024}, metadata(Mem)), 486 | ?assertEqual(a, peek_front(Mem)), 487 | ?assertEqual(a, peek_back(Mem)), 488 | 489 | ?assertEqual(ok, push(Mem, b)), 490 | ?assertEqual(a, peek_front(Mem)), 491 | ?assertEqual(b, peek_back(Mem)), 492 | 493 | ?assertEqual([], [R || R <- [push(Mem, I) || I <- [c,1,2,3]], R /= ok]), 494 | 495 | ?assertEqual([a,b,c,1,2,3], lists:reverse(peek(Mem, [], fun(I,S) -> {cont, [I | S]} end))), 496 | 497 | ?assertEqual({16,106,106,1024}, header(Mem)), 498 | ?assertEqual(6, length(Mem)), 499 | 500 | % Dequeue data 501 | ?assertMatch({a, false}, read_head(Mem, true)), 502 | ?assertEqual(b, pop(Mem)), 503 | ?assertEqual(c, pop(Mem)), 504 | ?assertEqual(1, pop(Mem)), 505 | ?assertMatch(2, peek_front(Mem)), 506 | ?assertMatch(2, try_pop(Mem, fun(2) -> 2 end)), 507 | ?assertError(failed, try_pop(Mem, fun(3) -> erlang:error(failed) end)), 508 | ?assertMatch({3, true}, read_head(Mem, true)), 509 | ?assertEqual(nil, pop(Mem)), 510 | ?assertEqual({106,106,106,1024}, header(Mem)), 511 | ?assert(purge(Mem)), 512 | ?assert(is_empty(Mem)), 513 | 514 | ?assertEqual([], [R || R <- [push(Mem, I) || I <- [a,b,1,2]], R /= ok]), 515 | ?assertEqual([a,b,1,2], lists:reverse(pop(Mem, [], fun(I,S) -> {cont, [I | S]} end))), 516 | ?assert(is_empty(Mem)), 517 | Term = string:copies("x", 128), 518 | ?assertEqual(ok, push(Mem, Term, 9)), 519 | ?assertEqual(ok, push(Mem, Term, 6)), 520 | ?assertEqual(ok, push(Mem, Term, 1)), 521 | ?assertEqual([Term, Term, Term], lists:reverse(pop(Mem, [], fun(I,S) -> {cont, [I | S]} end))), 522 | ?assertEqual(ok, flush(Mem)), 523 | ?assert(is_empty(Mem)), 524 | ?assertEqual(ok, push(Mem, Term, 1)), 525 | ?assertEqual(ok, erase(Mem)), 526 | ?assert(is_empty(Mem)). 527 | 528 | file_size() -> file_size(element(2, os:type())). 529 | file_size(darwin) -> 2048; 530 | file_size(_) -> 512. 531 | 532 | check_size(Sz) -> check_size(element(2,os:type()), Sz). 533 | check_size(darwin, _Sz) -> 2048; 534 | check_size(_, Sz) -> Sz. 535 | 536 | gen_server_queue_test() -> 537 | Filename = <<"/tmp/queue2.bin">>, 538 | {ok, Pid} = start_link(?MODULE, Filename, file_size(), [auto_unlink]), 539 | ok = enqueue(Pid, a), 540 | ?assert(filelib:is_regular(Filename)), 541 | ok = enqueue(Pid, b), 542 | ok = enqueue(Pid, c), 543 | 544 | ?assertEqual(a, dequeue(Pid)), 545 | ?assertEqual(b, inspect(Pid)), 546 | ?assertEqual(b, dequeue(Pid)), 547 | ?assertError(failed, try_dequeue(Pid, fun(c) -> erlang:error(failed) end)), 548 | ?assertEqual(c, try_dequeue(Pid, fun(c) -> c end)), 549 | ?assertEqual(nil, dequeue(Pid)), 550 | 551 | Blob = list_to_binary(string:copies("x", 256)), 552 | ?assertEqual(ok, enqueue(Pid, {1, Blob})), 553 | ?assertEqual(#{size => check_size(512), head => 16,next_tail => 292,tail => 292}, info(Pid)), 554 | ?assertEqual(ok, enqueue(Pid, {2, Blob})), 555 | ?assertEqual(#{size => check_size(1024),head => 16,next_tail => 568,tail => 568}, info(Pid)), 556 | ?assertEqual(ok, enqueue(Pid, {3, Blob})), 557 | ?assertEqual(#{size => check_size(1024),head => 16,next_tail => 844,tail => 844}, info(Pid)), 558 | ?assertEqual(ok, enqueue(Pid, {4, Blob})), 559 | ?assertEqual(#{size => 2048,head => 16,next_tail =>1120,tail =>1120}, info(Pid)), 560 | 561 | ?assertEqual({1,Blob}, dequeue(Pid)), 562 | ?assertEqual(#{size => 2048, head => 292,next_tail => 1120,tail => 1120}, info(Pid)), 563 | ?assertEqual({2,Blob}, dequeue(Pid)), 564 | ?assertEqual(#{size => 2048, head => 568,next_tail => 1120,tail => 1120}, info(Pid)), 565 | ?assertEqual({3,Blob}, dequeue(Pid)), 566 | ?assertEqual(#{size => 2048, head => 844,next_tail => 1120,tail => 1120}, info(Pid)), 567 | ?assertEqual({4,Blob}, dequeue(Pid)), 568 | ?assertEqual(#{size => 2048, head => 16,next_tail => 16,tail => 16}, info(Pid)), 569 | ?assertEqual(nil, dequeue(Pid)), 570 | 571 | case os:type() of 572 | {_, darwin} -> 573 | ?assertEqual({error, full}, enqueue(Pid, string:copies("x", 4096))); 574 | _ -> 575 | ok 576 | end. 577 | 578 | -endif. 579 | -------------------------------------------------------------------------------- /src/emmap.erl: -------------------------------------------------------------------------------- 1 | -module(emmap). 2 | 3 | -export([ 4 | init/0, 5 | open/2, open/4, close/1, resize/1, resize/2, flush/1, 6 | pread/3, pwrite/3, read/2, read_line/1, position/2, 7 | patomic_add/3, patomic_sub/3, patomic_and/3, patomic_or/3, patomic_xor/3, 8 | patomic_xchg/3, patomic_cas/4, 9 | patomic_read_integer/2, patomic_write_integer/3 10 | ]). 11 | -export([open_counters/1, open_counters/2, close_counters/1]). 12 | -export([inc_counter/2, inc_counter/3, dec_counter/2, dec_counter/3]). 13 | -export([set_counter/3, read_counter/2]). 14 | 15 | -export([ 16 | init_block_storage/2, 17 | repair_block_storage/1, repair_block_storage/3, 18 | store_block/2, read_block/2, free_block/2, 19 | read_blocks/1, read_blocks/3 20 | ]). 21 | 22 | -export_type([resource/0, mmap_file/0, open_option/0, open_extra_info/0]). 23 | 24 | -on_load(init/0). 25 | 26 | -ifdef(TEST). 27 | -include_lib("eunit/include/eunit.hrl"). 28 | -endif. 29 | 30 | -include_lib("kernel/include/file.hrl"). 31 | 32 | -type open_option() :: 33 | anon 34 | | auto_unlink 35 | | create 36 | | debug 37 | | direct 38 | | fixed 39 | | hugetlb 40 | | huge_2mb 41 | | huge_1gb 42 | | lock 43 | | nocache 44 | | nolock 45 | | nonblock 46 | | noreserve 47 | | populate 48 | | private 49 | | read 50 | | shared 51 | | shared_validate 52 | | sync 53 | | truncate 54 | | uninitialized 55 | | write 56 | | fixed_size 57 | | {address, pos_integer()} 58 | | {chmod, pos_integer()} 59 | | {size, pos_integer()} 60 | | {max_inc_size, pos_integer()}. 61 | %% Options for opening a memory mapped file: 62 | %% 63 | %%
64 | %%
anon
65 | %%
Anonymous mapping. The mapping is not backed by any file; 66 | %% its contents are initialized to zero. The offset argument should be zero.
67 | %%
auto_unlink
68 | %%
Automatically delete the mapped file after the mapped data was garbage collected. 69 | %% This can be used when the mapped file is a file-based shared-memory area (e.g. `/dev/shm/...') 70 | %% and is mapped in `direct' mode to free the memory after the data was gc'd
71 | %%
create
72 | %%
Allow to create mmap file if it doesn't exist.
73 | %%
debug
74 | %%
Turn on debug printing in the NIF library.
75 | %%
direct
76 | %%
Read/pread operations do not copy memory, but rather use "resource binaries" that 77 | %% can change content if the underlying data is changed. This is the most performant, 78 | %% but also has other thread-safety implications when not using atomic operations.
79 | %%
fixed
80 | %%
Don't interpret addr as a hint: place the mapping at exactly that address. 81 | %% The implementation aligns the given address to a multiple of the page size.
82 | %%
lock
83 | %%
Use a semaphore (read/write lock) to control state changes internally in the NIF 84 | %% library. This is the default option.
85 | %%
nocache
86 | %%
Pages in this mapping are not retained in the kernel's memory cache. 87 | %% If the system runs low on memory, pages in MAP_NOCACHE mappings will be among the 88 | %% first to be reclaimed. NOTE: this option is only valid for Mac OS.
89 | %%
nolock
90 | %%
Don't use a semaphore (read/write lock) to control state changes internally in the NIF library
91 | %%
nonblock
92 | %%
Don't perform read-ahead: create page tables entries only for pages that are 93 | %% already present in RAM. Since Linux 2.6.23, this flag causes `populate' to 94 | %% do nothing.
95 | %%
noreserve
96 | %%
Do not reserve swap space for this mapping. When swap space is reserved, one has 97 | %% the guarantee that it is possible to modify the mapping.
98 | %%
populate
99 | %%
Populate (prefault) page tables for a mapping. For a file mapping, this causes 100 | %% read-ahead on the file. This will help to reduce blocking on page faults later.
101 | %%
private
102 | %%
Create a private copy-on-write mapping. Updates to the mapping are not visible to 103 | %% other OS or Erlang processes mapping the same file, and are not carried through to the 104 | %% underlying file.
105 | %%
hugetlb
106 | %%
Allocate the mapping using "huge" pages.
107 | %%
huge_2mb
108 | %%
Used in conjunction with `hugetlb' to select alternative hugetlb page size of 2MB
109 | %%
huge_1gb
110 | %%
Used in conjunction with `hugetlb' to select alternative hugetlb page size of 1GB
111 | %%
read
112 | %%
Open for reading (this is default).
113 | %%
shared
114 | %%
Share this mapping. Updates to the mapping are visible to other OS or Erlang processes 115 | %% mapping the same region, and (in the case of file-backed mappings) are carried through 116 | %% to the underlying file. May be used in combination with `sync' to precisely control 117 | %% when updates are carried through to the underlying file.
118 | %%
shared_validate
119 | %%
This flag provides the same behavior as `shared' except that `shared' mappings ignore 120 | %% unknown flags in flags. By contrast, when creating a mapping using `shared_validate', 121 | %% the kernel verifies all passed flags are known and fails the mapping with the error 122 | %% `eopnotsupp' for unknown flags. This mapping type is also required to be able to use 123 | %% some mapping flags (e.g., `sync')
124 | %%
sync
125 | %%
This flag is available only with the `shared_validate' mapping type; mappings of type 126 | %% `shared' will silently ignore this flag. This flag is supported only for files 127 | %% supporting DAX (direct mapping of persistent memory). For other files, creating a 128 | %% mapping with this flag results in an `eopnotsupp' error. 129 | %% Shared file mappings with this flag provide the guarantee that while some memory is 130 | %% mapped writable in the address space of the OS process, it will be visible in the same 131 | %% file at the same offset even after the system crashes or is rebooted. In conjunction 132 | %% with the use of appropriate CPU instructions, this provides users of such mappings 133 | %% with a more efficient way of making data modifications persistent.
134 | %%
truncate
135 | %%
Truncate existing mmap file after it's open.
136 | %%
uninitialized
137 | %%
Don't clear anonymous pages. This flag is intended to improve performance on 138 | %% embedded devices. This flag is honored only if the kernel was configured with 139 | %% the `CONFIG_MMAP_ALLOW_UNINITIALIZED' option.
140 | %%
write
141 | %%
Open memory map for writing.
142 | %%
fixed_size
143 | %%
Don't allow the memory to be resized
144 | %%
{address, pos_integer()}
145 | %%
Open mapping at the given memory address (sets `MAP_FIXED' on the memory mapped file)
146 | %%
{chmod, pos_integer()}
147 | %%
Create mmap file with this mode (default: `0600')
148 | %%
{size, pos_integer()}
149 | %%
Create/access memory map on this size.
150 | %%
{max_inc_size, pos_integer()}
151 | %%
Size threshold used when automatically resizing shared memory with call to `resize/1'. 152 | %% Below this threshold the memory will double, and after this threshold, the resized 153 | %% memory will be increased by `max_inc_size'.
154 | %%
155 | 156 | -type mmap_file() :: #file_descriptor{}. 157 | -type resource() :: binary(). 158 | 159 | -type open_extra_info() :: #{exist => boolean(), size => non_neg_integer()}. 160 | %% Extra information returned by the call to `emmap:open/2,3'. 161 | %% The value of `exist' true means that an existing memory map was open. The `size' 162 | %% represents the size of the memory map that was open. 163 | 164 | init() -> 165 | SoName = 166 | case code:priv_dir(emmap) of 167 | {error, bad_name} -> 168 | case code:which(?MODULE) of 169 | Filename when is_list(Filename) -> 170 | Dir = filename:dirname(filename:dirname(Filename)), 171 | filename:join([Dir, "priv", "emmap"]); 172 | _ -> 173 | filename:join("../priv", "emmap") 174 | end; 175 | Dir -> 176 | filename:join(Dir, "emmap") 177 | end, 178 | erlang:load_nif(SoName, 0). 179 | 180 | %% @doc Open/create a memory-mapped file. 181 | %% If creating a new file, `[create, read, write, {size, N}]' options are required. 182 | %% For opening an existing file for writing `[read, write]' options are required. 183 | -spec open(string()|binary(), [open_option()]) -> 184 | {ok, mmap_file(), open_extra_info()} | {error, term()}. 185 | open(FileName, Options) when is_binary(FileName) -> 186 | open(binary_to_list(FileName), Options); 187 | open(FileName, Options) when is_list(FileName) -> 188 | case file:read_file_info(FileName) of 189 | {ok, FileInfo} -> 190 | open(FileName, 0, nvl(FileInfo#file_info.size, 0), Options); 191 | _Error -> 192 | open(FileName, 0, 0, Options) 193 | end. 194 | 195 | %% @doc Open/create a memory-mapped file. 196 | %% If creating a new file, `[create, read, write]' options and the `Len' parameter 197 | %% are required. 198 | %% For opening an existing file for writing `[read, write]' options are required, and `Len' 199 | %% can be `0'. 200 | -spec open(File::string()|binary(), 201 | Offset::non_neg_integer(), 202 | Length::non_neg_integer(), 203 | Options::[ open_option() ]) -> 204 | {ok, mmap_file(), open_extra_info()} | {error, term()}. 205 | open(FileName, Off, Len, Options) when is_binary(FileName) -> 206 | open(binary_to_list(FileName), Off, Len, Options); 207 | open(FileName, Off, Len, Options) when is_list(FileName), is_integer(Off), is_integer(Len) -> 208 | case open_nif(FileName, Off, Len, Options) of 209 | {ok, Mem, Info} -> 210 | {ok, #file_descriptor{module=?MODULE, data=Mem}, Info}; 211 | {error, _} = Error -> 212 | Error 213 | end. 214 | 215 | open_nif(_,_,_,_) -> 216 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 217 | 218 | -spec close(File::mmap_file()) -> ok. 219 | close(#file_descriptor{module=?MODULE, data=Mem}) -> close_nif(Mem). 220 | close_nif(_) -> 221 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 222 | 223 | %% @doc Resize shared memory. 224 | %% The new size will double the existing size up until the `max_inc_size' threshold (passed 225 | %% to `open/4' (default 64M), otherwise incremented by `max_inc_size'. 226 | -spec resize(File::mmap_file()) -> {ok, NewSize::non_neg_integer()} | {error, atom()|string()}. 227 | resize(#file_descriptor{module=?MODULE, data=Mem}) -> resize_nif(Mem, 0). 228 | 229 | %% @doc Resize shared memory to a given new size. 230 | -spec resize(mmap_file(), non_neg_integer()) -> 231 | {ok, NewSize::non_neg_integer()} | {error, string()}. 232 | resize(#file_descriptor{module=?MODULE, data=Mem}, NewSize) when is_integer(NewSize) -> 233 | resize_nif(Mem, NewSize). 234 | 235 | resize_nif(_, _) -> 236 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 237 | 238 | %% @doc Ask the OS to flush the modified memory to disk. The call is asyncronous and 239 | %% non-blocking. This call is not required as the OS will asynchronously flush the 240 | %% modified memory pages to disk lazily, but this call will trigger that process 241 | %% immediately. 242 | -spec flush(File::mmap_file()) -> ok. 243 | flush(#file_descriptor{module=?MODULE, data=Mem}) -> sync_nif(Mem). 244 | sync_nif(_) -> 245 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 246 | 247 | %% @doc Read `Len' bytes from a memory-mapped file at a given offset `Off'. 248 | -spec pread(File::mmap_file(), Offset::non_neg_integer(), Length::non_neg_integer()) -> 249 | {ok, binary()} | {error, term()} | eof. 250 | 251 | pread(#file_descriptor{module=?MODULE, data=Mem}, Off, Len) -> 252 | pread_nif(Mem, Off, Len). 253 | pread_nif(_,_,_) -> 254 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 255 | 256 | %% @doc Read next `Len' bytes from a memory-mapped file. 257 | %% Internally the new position within the file is incremented by `Len'. 258 | -spec read(File::mmap_file(), Length::non_neg_integer()) -> 259 | {ok, binary()} | {error, term()} | eof. 260 | 261 | read(#file_descriptor{module=?MODULE, data=Mem}, Len) -> read_nif(Mem, Len). 262 | read_nif(_,_) -> 263 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 264 | 265 | 266 | -spec read_line(File::mmap_file()) -> {ok, binary()} | {error, term()} | eof. 267 | 268 | read_line(#file_descriptor{module=?MODULE, data=Mem}) -> read_line_nif(Mem). 269 | read_line_nif(_) -> 270 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 271 | 272 | %% @doc Write `Data' bytes to a memory-mapped file at a given offset `Off'. 273 | -spec pwrite(File::mmap_file(), Position::non_neg_integer(), Data::binary()) -> 274 | ok | {error, term()}. 275 | pwrite(#file_descriptor{module=?MODULE, data=Mem}, Off, Data) -> pwrite_nif(Mem, Off, Data). 276 | pwrite_nif(_,_,_) -> 277 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 278 | 279 | %% @doc Write `Data' bytes to a memory-mapped file at a given offset `At'. 280 | -spec position(File::mmap_file(), 281 | Position::non_neg_integer() | {bof|cur|eof, Position::integer()} ) -> 282 | {ok, non_neg_integer()} | {error, term()}. 283 | position(#file_descriptor{module=?MODULE, data=Mem}, At) 284 | when is_integer(At) -> 285 | position_nif(Mem, bof, At); 286 | position(#file_descriptor{module=?MODULE, data=Mem}, From) 287 | when From == 'bof'; From == 'cur'; From == 'eof' -> 288 | position_nif(Mem, From, 0); 289 | position(#file_descriptor{module=?MODULE, data=Mem}, {From, Off}) 290 | when From == 'bof'; From == 'cur'; From == 'eof' -> 291 | position_nif(Mem, From, Off). 292 | 293 | position_nif(_,_From,_Off) -> 294 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 295 | 296 | %% @doc Perform an atomic ADD operation on a 64-bit integer value at given `Position' 297 | %% using specified argument `Value'. The function returns an old value at that 298 | %% location. This function is thread-safe and can be used for implementing 299 | %% persistent counters. 300 | -spec patomic_add(File::mmap_file(), Position::non_neg_integer(), Value::integer()) -> 301 | {ok, OldValue::integer()} | {error, atom()} | no_return(). 302 | patomic_add(#file_descriptor{module=?MODULE, data=Mem}, Off, Value) 303 | when is_integer(Off), is_integer(Value) -> 304 | patomic_add_nif(Mem, Off, Value). 305 | 306 | patomic_add_nif(_,_,_) -> 307 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 308 | 309 | %% @doc Perform an atomic SUBTRACT operation on a 64-bit integer value at given `Position' 310 | %% using specified argument `Value'. The function returns an old value at that 311 | %% location. This function is thread-safe and can be used for implementing 312 | %% persistent counters. 313 | -spec patomic_sub(File::mmap_file(), Position::non_neg_integer(), Value::integer()) -> 314 | {ok, OldValue::integer()} | {error, atom()} | no_return(). 315 | patomic_sub(#file_descriptor{module=?MODULE, data=Mem}, Off, Value) 316 | when is_integer(Off), is_integer(Value) -> 317 | patomic_sub_nif(Mem, Off, Value). 318 | 319 | patomic_sub_nif(_,_,_) -> 320 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 321 | 322 | %% @doc Perform an atomic AND operation on a 64-bit integer value at given `Position' 323 | %% using specified argument `Value'. The function returns an AND'd value at that 324 | %% location. 325 | -spec patomic_and(File::mmap_file(), Position::non_neg_integer(), Value::integer()) -> 326 | {ok, integer()} | {error, atom()} | no_return(). 327 | patomic_and(#file_descriptor{module=?MODULE, data=Mem}, Off, Value) 328 | when is_integer(Off), is_integer(Value) -> 329 | patomic_and_nif(Mem, Off, Value). 330 | 331 | patomic_and_nif(_,_,_) -> 332 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 333 | 334 | %% @doc Perform an atomic OR operation on a 64-bit integer value at given `Position' 335 | %% using specified argument `Value'. The function returns an OR'd value at that 336 | %% location. 337 | -spec patomic_or(File::mmap_file(), Position::non_neg_integer(), Value::integer()) -> 338 | {ok, integer()} | {error, atom()} | no_return(). 339 | patomic_or(#file_descriptor{module=?MODULE, data=Mem}, Off, Value) 340 | when is_integer(Off), is_integer(Value) -> 341 | patomic_or_nif(Mem, Off, Value). 342 | 343 | patomic_or_nif(_,_,_) -> 344 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 345 | 346 | %% @doc Perform an atomic XOR operation on a 64-bit integer value at given `Position' 347 | %% using specified argument `Value'. The function returns an XOR'd value at that 348 | %% location. 349 | -spec patomic_xor(File::mmap_file(), Position::non_neg_integer(), Value::integer()) -> 350 | {ok, integer()} | {error, atom()} | no_return(). 351 | patomic_xor(#file_descriptor{module=?MODULE, data=Mem}, Off, Value) 352 | when is_integer(Off), is_integer(Value) -> 353 | patomic_xor_nif(Mem, Off, Value). 354 | 355 | patomic_xor_nif(_,_,_) -> 356 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 357 | 358 | %% @doc Perform an atomic EXCHANGE operation on a 64-bit integer value at given `Position' 359 | %% using specified argument `Value'. The function returns an old value at that 360 | %% location. 361 | -spec patomic_xchg(File::mmap_file(), Position::non_neg_integer(), Value::integer()) -> 362 | {ok, integer()} | {error, atom()} | no_return(). 363 | patomic_xchg(#file_descriptor{module=?MODULE, data=Mem}, Off, Value) 364 | when is_integer(Off), is_integer(Value) -> 365 | patomic_xchg_nif(Mem, Off, Value). 366 | 367 | patomic_xchg_nif(_,_,_) -> 368 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 369 | 370 | %% @doc Perform an atomic compare and swap (CAS) operation on a 64-bit integer value 371 | %% at given `Position' using specified argument `Value'. The function returns a 372 | %% tuple `{Success, OldVal}', where `OldVal' is the old value at that location. 373 | -spec patomic_cas(File::mmap_file(), Position::non_neg_integer(), integer(), integer()) -> 374 | {boolean(), integer()} | {error, atom()} | no_return(). 375 | patomic_cas(#file_descriptor{module=?MODULE, data=Mem}, Off, OldValue, Value) 376 | when is_integer(Off), is_integer(OldValue), is_integer(Value) -> 377 | patomic_cas_nif(Mem, Off, OldValue, Value). 378 | 379 | patomic_cas_nif(_,_,_,_) -> 380 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 381 | 382 | %% @doc Perform an atomic store operation of a 64-bit integer `Value' at given `Position'. 383 | %% This function is thread-safe and can be used for implementing persistent counters. 384 | -spec patomic_write_integer(File::mmap_file(), Position::non_neg_integer(), Value::integer()) -> ok. 385 | patomic_write_integer(#file_descriptor{module=?MODULE, data=Mem}, Off, Value) 386 | when is_integer(Off), is_integer(Value) -> 387 | patomic_write_int_nif(Mem, Off, Value). 388 | 389 | patomic_write_int_nif(_,_,_) -> 390 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 391 | 392 | %% @doc Perform an atomic load operation on a 64-bit integer value at given `Position'. 393 | %% This function is thread-safe and can be used for implementing persistent counters. 394 | -spec patomic_read_integer(File::mmap_file(), Position::non_neg_integer()) -> Value::integer(). 395 | patomic_read_integer(#file_descriptor{module=?MODULE, data=Mem}, Off) when is_integer(Off) -> 396 | patomic_read_int_nif(Mem, Off). 397 | 398 | patomic_read_int_nif(_,_) -> 399 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 400 | 401 | %% @doc Open a persistent memory-mapped file with space for one 64-bit integer counter 402 | open_counters(Filename) -> 403 | open_counters(Filename, 1). 404 | 405 | %% @doc Open a persistent memory-mapped file with space for several 64-bit integer counters 406 | open_counters(Filename, NumCounters) -> 407 | Size = 8 + 8 + NumCounters * 8, 408 | FileSize = filelib:file_size(Filename), 409 | MMAP = 410 | case open(Filename, 0, Size, [create, read, write, shared, direct, nolock]) of 411 | {ok, F, #{exist := false}} when NumCounters > 0 -> 412 | ok = pwrite(F, 0, <<"EMMAP01\n", NumCounters:64/integer-little>>), 413 | {F, NumCounters}; 414 | {ok, F, #{exist := true}} -> 415 | case pread(F, 0, 16) of 416 | {ok, <<"EMMAP", _,_,$\n, N:64/integer-little>>} when N == NumCounters; NumCounters == 0 -> 417 | {F, N}; 418 | {ok, _Other} when FileSize == Size -> 419 | % io:format("Initializing mmap: ~p\n", [_Other]), 420 | ok = pwrite(F, 0, <<"EMMAP01\n", NumCounters:64/integer-little>>), 421 | lists:foldl(fun(I, _) -> 422 | ok = pwrite(F, 16+I*8, <<0:64/integer-little>>), 423 | [] 424 | end, [], lists:seq(0, NumCounters-1)), 425 | {F, NumCounters}; 426 | {ok, Header} -> 427 | erlang:error({invalid_file_header, Filename, Header}); 428 | {error, Why} -> 429 | erlang:error({cannot_read_data, Filename, Why}) 430 | end; 431 | {error, Reason} -> 432 | erlang:error({cannot_open_file, Filename, Reason}) 433 | end, 434 | MMAP. 435 | 436 | %% @doc Close persistent memory-mapped file previously open with `open_counters/2' 437 | close_counters({MFile, _NumCnts}) -> 438 | close(MFile). 439 | 440 | %% @doc Increment a counter number `CounterNum' in the mmap file by one and return old value. 441 | inc_counter({MFile, NumCnts}, CounterNum) -> 442 | inc_counter({MFile, NumCnts}, CounterNum, 1). 443 | 444 | %% @doc Decrement a counter number `CounterNum' in the mmap file by one and return old value. 445 | dec_counter({MFile, NumCnts}, CounterNum) -> 446 | dec_counter({MFile, NumCnts}, CounterNum, 1). 447 | 448 | %% @doc Increment a counter number `CounterNum' in the mmap file by `Count' and return old value. 449 | inc_counter({MFile, NumCnts}, CounterNum, Count) 450 | when NumCnts > CounterNum, CounterNum >= 0, is_integer(CounterNum), is_integer(Count) -> 451 | patomic_add(MFile, 16+CounterNum*8, Count). 452 | 453 | %% @doc Decrement a counter number `CounterNum' in the mmap file by `Count' and return old value. 454 | dec_counter({MFile, NumCnts}, CounterNum, Count) 455 | when NumCnts > CounterNum, CounterNum >= 0, is_integer(CounterNum), is_integer(Count) -> 456 | patomic_sub(MFile, 16+CounterNum*8, Count). 457 | 458 | %% @doc Set a counter number `CounterNum' in the mmap file and return the old value. 459 | set_counter({MFile, NumCnts}, CounterNum, Value) 460 | when NumCnts > CounterNum, CounterNum >= 0, is_integer(CounterNum), is_integer(Value) -> 461 | patomic_xchg(MFile, 16+CounterNum*8, Value). 462 | 463 | read_counter({MFile, NumCnts}, CounterNum) 464 | when NumCnts > CounterNum, CounterNum >= 0, is_integer(CounterNum) -> 465 | patomic_read_integer(MFile, 16+CounterNum*8). 466 | 467 | nvl(undefined, V) -> V; 468 | nvl(V, _) -> V. 469 | 470 | %% @doc Initialize fixed-size block storage in the shared memory. 471 | -spec init_block_storage(File::mmap_file(), BlockSize::pos_integer()) -> ok | {error, atom()|string()}. 472 | init_block_storage(#file_descriptor{module=?MODULE, data=Mem}, BlockSize) when is_integer(BlockSize) -> 473 | init_bs_nif(Mem, BlockSize). 474 | 475 | init_bs_nif(_,_) -> 476 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 477 | 478 | %% @doc Store data block 479 | -spec store_block(File::mmap_file(), Data::binary()) -> Addr::non_neg_integer() | {error, atom()|string()}. 480 | store_block(#file_descriptor{module=?MODULE, data=Mem}, Data) when is_binary(Data) -> 481 | store_blk_nif(Mem, Data). 482 | 483 | store_blk_nif(_,_) -> 484 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 485 | 486 | %% @doc Read data block 487 | -spec read_block(File::mmap_file(), Addr::non_neg_integer()) -> Data::binary() | eof | {error, atom()|string()}. 488 | read_block(#file_descriptor{module=?MODULE, data=Mem}, Addr) when is_integer(Addr), Addr >= 0 -> 489 | read_blk_nif(Mem, Addr). 490 | 491 | read_blk_nif(_,_) -> 492 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 493 | 494 | %% @doc Read data blocks 495 | -spec read_blocks(File::mmap_file()) -> [Data::binary()] | eof | {error, atom()|string()}. 496 | read_blocks(#file_descriptor{module=?MODULE, data=Mem}) -> 497 | read_blocks_nif(Mem). 498 | 499 | read_blocks_nif(_) -> 500 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 501 | 502 | %% @doc Read data blocks 503 | -spec read_blocks(File::mmap_file(), Start::non_neg_integer(), Count::pos_integer()) -> 504 | {[Data::binary()], Continuation::integer() | eof} | {error, atom()|string()}. 505 | read_blocks(#file_descriptor{module=?MODULE, data=Mem}, Start, Count) -> 506 | read_blocks_nif(Mem, Start, Count). 507 | 508 | read_blocks_nif(_, _, _) -> 509 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 510 | 511 | %% @doc Free data block 512 | -spec free_block(File::mmap_file(), Addr::non_neg_integer()) -> Success :: boolean() | {error, atom()|string()}. 513 | free_block(#file_descriptor{module=?MODULE, data=Mem}, Addr) when is_integer(Addr), Addr >= 0 -> 514 | free_blk_nif(Mem, Addr). 515 | 516 | free_blk_nif(_,_) -> 517 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 518 | 519 | %% @doc Repair data block storage after unexpected close 520 | -spec repair_block_storage(File::mmap_file()) -> ok | {error, atom()|string()}. 521 | repair_block_storage(#file_descriptor{module=?MODULE, data=Mem}) -> 522 | repair_bs_nif(Mem). 523 | 524 | repair_bs_nif(_) -> 525 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 526 | 527 | %% @doc Repair data block storage chunk of max Count elements starting with Start address 528 | -spec repair_block_storage(File::mmap_file(), Start::non_neg_integer(), Count::pos_integer()) -> 529 | Continuation::integer() | eof | {error, atom()|string()}. 530 | repair_block_storage(#file_descriptor{module=?MODULE, data=Mem}, Start, Count) -> 531 | repair_bs_nif(Mem, Start, Count). 532 | 533 | repair_bs_nif(_, _, _) -> 534 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]}). 535 | 536 | -ifdef(EUNIT). 537 | 538 | simple_test() -> 539 | {ok, File} = file:open("test.data", [raw, write]), 540 | ok = file:write(File, <<"abcd0123">>), 541 | ok = file:close(File), 542 | 543 | %% with direct+shared, the contents of a binary may change 544 | {ok, MFile, #{size := 8}} = emmap:open("test.data", 0, 8, [direct, shared, nolock]), 545 | {ok, Mem} = file:pread(MFile, 2, 2), 546 | <<"cd">> = Mem, 547 | {error, eacces} = file:pwrite(MFile, 2, <<"xx">>), 548 | 549 | {ok, MFile2, #{size := 8}} = emmap:open("test.data", 0, 8, [read, write, shared]), 550 | ok = file:pwrite(MFile2, 2, <<"xx">>), 551 | {ok, <<"xx">>} = file:pread(MFile, 2, 2), 552 | 553 | %% Woot! 554 | <<"xx">> = Mem, 555 | 556 | {ok, 0} = file:position(MFile, {cur, 0}), 557 | {ok, <<"ab">>} = file:read(MFile, 2), 558 | {ok, <<"xx">>} = file:read(MFile, 2), 559 | 560 | ok = file:pwrite(MFile2, 0, <<0:64>>), 561 | {ok, <<0:64>>} = file:pread(MFile, 0, 8), 562 | 563 | {ok, 0} = emmap:patomic_add(MFile2, 0,10), 564 | {ok, 10} = emmap:patomic_add(MFile2, 0,10), 565 | {ok, 20} = emmap:patomic_sub(MFile2, 0, 5), 566 | {ok, 15} = emmap:patomic_sub(MFile2, 0,12), 567 | {ok, 3} = emmap:patomic_and(MFile2, 0, 7), 568 | {ok, 3} = emmap:patomic_or(MFile2, 0, 7), 569 | {ok, 7} = emmap:patomic_xor(MFile2, 0, 9), 570 | {ok, 14} = emmap:patomic_xchg(MFile2, 0,10), 571 | {ok, 10} = emmap:patomic_xchg(MFile2, 0, 0), 572 | {true, 0} = emmap:patomic_cas(MFile2, 0, 0, 20), 573 | {false, 20} = emmap:patomic_cas(MFile2, 0, 10, 20), 574 | {true, 20} = emmap:patomic_cas(MFile2, 0, 20, 0), 575 | 576 | file:close(MFile), 577 | file:close(MFile2), 578 | 579 | {ok, MFile3, #{exist := true, size := 8}} = emmap:open("test.data", 0, 8, 580 | [direct, read, write, shared, nolock, {address, 16#512800000000}]), 581 | {ok, <<0:64>>} = file:pread(MFile3, 0, 8), 582 | file:close(MFile3), 583 | 584 | file:delete("test.data"). 585 | 586 | counter_test() -> 587 | F = open_counters("/tmp/temp.bin", 1), 588 | {ok,N1} = inc_counter(F, 0, 1), 589 | {ok,N2} = inc_counter(F, 0, 1), 590 | {ok,N3} = set_counter(F, 0, 5), 591 | {ok,N4} = set_counter(F, 0, 8), 592 | {ok,N5} = read_counter(F, 0), 593 | {ok,N6} = dec_counter(F, 0), 594 | {ok,N7} = dec_counter(F, 0, 3), 595 | {ok,N8} = read_counter(F, 0), 596 | close_counters(F), 597 | file:delete("/tmp/temp.bin"), 598 | ?assertEqual(0, N1), 599 | ?assertEqual(1, N2), 600 | ?assertEqual(2, N3), 601 | ?assertEqual(5, N4), 602 | ?assertEqual(8, N5), 603 | ?assertEqual(8, N6), 604 | ?assertEqual(7, N7), 605 | ?assertEqual(4, N8). 606 | 607 | shared_test() -> 608 | F = fun(Owner) -> 609 | {ok, MM, #{size := 8}} = emmap:open("test.data", 0, 8, [create, direct, read, write, shared, nolock]), 610 | Two = receive {start, PP} -> PP end, 611 | ok = emmap:pwrite(MM, 0, <<"test1">>), 612 | Two ! {self(), <<"test1">>}, 613 | receive {cont, Two} -> ok end, 614 | ok = emmap:pwrite(MM, 0, <<"test2">>), 615 | Two ! {self(), <<"test2">>}, 616 | receive {cont, Two} -> ok end, 617 | Two ! {done, 1}, 618 | Owner ! {done, MM} 619 | end, 620 | G = fun(One, Owner) -> 621 | {ok, MM, #{size := 8}} = emmap:open("test.data", 0, 8, [create, direct, read, write, shared, nolock]), 622 | One ! {start, self()}, 623 | Bin = 624 | receive 625 | {One, Bin1 = <<"test1">>} -> 626 | {ok, B} = emmap:pread(MM, 0, byte_size(Bin1)), 627 | B; 628 | Other1 -> 629 | erlang:error({error, {one, Other1}}) 630 | end, 631 | % At this point value of Bin is this: 632 | Bin = <<"test1">>, 633 | One ! {cont, self()}, 634 | receive 635 | {One, Bin2 = <<"test2">>} when Bin2 == Bin -> 636 | % Note that previously bound binary changed under the hood 637 | % because it's bound to the memory updated by another process 638 | Bin = <<"test2">>; 639 | Other2 -> 640 | erlang:error({error, {two, Other2}}) 641 | end, 642 | One ! {cont, self()}, 643 | receive 644 | {done, 1} -> ok 645 | end, 646 | Owner ! {done, MM} 647 | end, 648 | Self = self(), 649 | P1 = spawn_link(fun() -> F(Self) end), 650 | _ = spawn_link(fun() -> G(P1, Self) end), 651 | 652 | receive {done, MM1} -> emmap:close(MM1) end, 653 | receive {done, MM2} -> emmap:close(MM2) end, 654 | 655 | file:delete("test.data"). 656 | 657 | 658 | -endif. 659 | -------------------------------------------------------------------------------- /c_src/emmap.cpp: -------------------------------------------------------------------------------- 1 | // vim:ts=2:sw=2:et 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "bsna.hpp" 16 | 17 | static ErlNifResourceType* MMAP_RESOURCE; 18 | 19 | #ifndef MAP_NOCACHE 20 | /* No MAP_NOCACHE on Linux - just bypass this option */ 21 | # define MAP_NOCACHE (0) 22 | #endif 23 | 24 | // MAP_ANON is deprecated on Linux, and MAP_ANONYMOUS is not present on Mac 25 | #ifndef MAP_ANONYMOUS 26 | # define MAP_ANONYMOUS MAP_ANON 27 | #endif 28 | 29 | #if (__GLIBC__ == 2 && __GLIBC_MINOR >= 32) || (__GLIBC__ > 2) 30 | # define strerror_compat strerrordesc_np 31 | #else 32 | # define strerror_compat strerror 33 | #endif 34 | 35 | template 36 | static void debug(bool dbg, const char* fmt, Args&&... args) 37 | { 38 | if (dbg) 39 | fprintf(stderr, fmt, std::forward(args)...); 40 | } 41 | 42 | struct mhandle 43 | { 44 | mhandle(void* mem, const char* file, int fd, size_t size, bool direct, 45 | bool fixed_size, size_t max_inc_size, 46 | bool auto_unlink, int prot, bool lock, bool dbg) 47 | : position(0) 48 | , fd(fd) 49 | , direct(direct) 50 | , prot(prot) 51 | , dbg(dbg) 52 | , rwlock(lock ? enif_rwlock_create((char*)"mmap") : nullptr) 53 | , mem(mem) 54 | , direct_mem() 55 | , len(size) 56 | , max_inc_size(max_inc_size) 57 | , fixed_size(fixed_size) 58 | , auto_unlink(auto_unlink) 59 | { 60 | snprintf(path, sizeof(path), "%s", file); 61 | } 62 | 63 | ~mhandle() 64 | { 65 | unmap(true); 66 | 67 | if (direct) { 68 | for (auto& slot : direct_mem) { 69 | debug(dbg, "Unmapping direct memory %p of size %lu\r\n", slot.first, slot.second); 70 | munmap(slot.first, slot.second); 71 | } 72 | direct_mem.clear(); 73 | } 74 | 75 | if (rwlock != 0) 76 | enif_rwlock_destroy(rwlock); 77 | } 78 | 79 | bool unmap(bool destruct) 80 | { 81 | auto result = true; 82 | 83 | if (mem) { 84 | if (destruct || !direct) { 85 | result = munmap(mem, len) == 0; 86 | debug(dbg, "Releasing memory map %p of size %lu -> %s\r\n", mem, len, 87 | result ? "ok" : strerror_compat(errno)); 88 | } else { 89 | // We must not unmap the memory that might be in use by some binaries 90 | // until the destructor is called. 91 | debug(dbg, "Preserving memory map %p of size %lu\r\n", mem, len); 92 | direct_mem.push_back(MemSlot(mem, len)); 93 | } 94 | 95 | mem = nullptr; 96 | } 97 | 98 | if (auto_unlink && path[0] != '\0') { 99 | debug(dbg, "Removing memory mapped file %s\r\n", path); 100 | unlink(path); 101 | path[0] = '\0'; 102 | } 103 | 104 | return result; 105 | } 106 | 107 | bool closed() const { return mem == nullptr; } 108 | 109 | using MemSlot = std::pair; 110 | using MemList = std::list; 111 | 112 | size_t position; 113 | int fd; 114 | bool direct; 115 | int prot; 116 | bool dbg; 117 | ErlNifRWLock* rwlock; 118 | void* mem; 119 | MemList direct_mem; // Stash of allocated mem segments used in direct mode 120 | size_t len; 121 | size_t max_inc_size; // Below this threshold, when resizing, the size will double 122 | bool fixed_size; // Don't allow memory to be resized 123 | bool auto_unlink; 124 | char path[1024]; 125 | }; 126 | 127 | template 128 | static void debug(mhandle* h, const char* fmt, Args&&... args) 129 | { 130 | debug(h->dbg, fmt, std::forward(args)...); 131 | } 132 | 133 | static int on_load(ErlNifEnv*, void**, ERL_NIF_TERM); 134 | static int on_upgrade(ErlNifEnv* env, void** priv_data, void**, ERL_NIF_TERM load_info); 135 | 136 | void emmap_dtor(ErlNifEnv* env, void* arg) 137 | { 138 | assert(arg); 139 | static_cast(arg)->~mhandle(); // Call the destructor 140 | } 141 | 142 | #define RW_UNLOCK if (handle->rwlock != 0) enif_rwlock_rwunlock(handle->rwlock) 143 | #define RW_LOCK if (handle->rwlock != 0) enif_rwlock_rwlock(handle->rwlock) 144 | #define R_UNLOCK if (handle->rwlock != 0) enif_rwlock_runlock(handle->rwlock) 145 | #define R_LOCK if (handle->rwlock != 0) enif_rwlock_rlock(handle->rwlock) 146 | 147 | struct rw_lock { 148 | rw_lock(mhandle* h) : lock(h->rwlock) { if (lock != 0) enif_rwlock_rwlock(lock); } 149 | rw_lock(const rw_lock&) = delete; 150 | rw_lock& operator=(const rw_lock&) = delete; 151 | ~rw_lock() { if (lock != 0) enif_rwlock_rwunlock(lock); } 152 | ErlNifRWLock* lock; 153 | }; 154 | 155 | struct r_lock { 156 | r_lock(mhandle* h) : lock(h->rwlock) { if (lock != 0) enif_rwlock_rlock(lock); } 157 | r_lock(const r_lock&) = delete; 158 | r_lock& operator=(const r_lock&) = delete; 159 | ~r_lock() { if (lock != 0) enif_rwlock_runlock(lock); } 160 | ErlNifRWLock* lock; 161 | }; 162 | 163 | static ERL_NIF_TERM ATOM_ADDRESS; 164 | static ERL_NIF_TERM ATOM_ALIGNMENT; 165 | static ERL_NIF_TERM ATOM_ANON; 166 | static ERL_NIF_TERM ATOM_AUTO_UNLINK; 167 | static ERL_NIF_TERM ATOM_BOF; 168 | static ERL_NIF_TERM ATOM_CANNOT_CREATE_MAP; 169 | static ERL_NIF_TERM ATOM_CHMOD; 170 | static ERL_NIF_TERM ATOM_CLOSED; 171 | static ERL_NIF_TERM ATOM_CREATE; 172 | static ERL_NIF_TERM ATOM_CUR; 173 | static ERL_NIF_TERM ATOM_DEBUG; 174 | static ERL_NIF_TERM ATOM_DIRECT; 175 | static ERL_NIF_TERM ATOM_EACCES; 176 | static ERL_NIF_TERM ATOM_ENOMEM; 177 | static ERL_NIF_TERM ATOM_EOF; 178 | static ERL_NIF_TERM ATOM_ERROR; 179 | static ERL_NIF_TERM ATOM_EXIST; 180 | static ERL_NIF_TERM ATOM_FALSE; 181 | static ERL_NIF_TERM ATOM_FILE; 182 | static ERL_NIF_TERM ATOM_FIXED; 183 | static ERL_NIF_TERM ATOM_FIXED_SIZE; 184 | static ERL_NIF_TERM ATOM_FIT; 185 | static ERL_NIF_TERM ATOM_FULL; 186 | static ERL_NIF_TERM ATOM_HUGETLB; 187 | static ERL_NIF_TERM ATOM_HUGE_2MB; 188 | static ERL_NIF_TERM ATOM_HUGE_1GB; 189 | static ERL_NIF_TERM ATOM_LOCK; 190 | static ERL_NIF_TERM ATOM_MAX_INC_SIZE; 191 | static ERL_NIF_TERM ATOM_NOCACHE; 192 | static ERL_NIF_TERM ATOM_NOLOCK; 193 | static ERL_NIF_TERM ATOM_NONBLOCK; 194 | static ERL_NIF_TERM ATOM_NONE; 195 | static ERL_NIF_TERM ATOM_NORESERVE; 196 | static ERL_NIF_TERM ATOM_OK; 197 | static ERL_NIF_TERM ATOM_POPULATE; 198 | static ERL_NIF_TERM ATOM_PRIVATE; 199 | static ERL_NIF_TERM ATOM_READ; 200 | static ERL_NIF_TERM ATOM_SHARED; 201 | #ifndef __APPLE__ 202 | static ERL_NIF_TERM ATOM_SHARED_VALIDATE; 203 | #endif 204 | static ERL_NIF_TERM ATOM_SIZE; 205 | #ifndef __APPLE__ 206 | static ERL_NIF_TERM ATOM_SYNC; 207 | #endif 208 | static ERL_NIF_TERM ATOM_TRUE; 209 | static ERL_NIF_TERM ATOM_TRUNCATE; 210 | static ERL_NIF_TERM ATOM_UNINITIALIZED; 211 | static ERL_NIF_TERM ATOM_WRITE; 212 | 213 | static ERL_NIF_TERM emmap_open (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 214 | static ERL_NIF_TERM emmap_resize (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 215 | static ERL_NIF_TERM emmap_sync (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 216 | static ERL_NIF_TERM emmap_read (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 217 | static ERL_NIF_TERM emmap_read_line (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 218 | static ERL_NIF_TERM emmap_close (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 219 | static ERL_NIF_TERM emmap_pread (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 220 | static ERL_NIF_TERM emmap_pwrite (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 221 | static ERL_NIF_TERM emmap_position (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 222 | static ERL_NIF_TERM emmap_patomic_add (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 223 | static ERL_NIF_TERM emmap_patomic_sub (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 224 | static ERL_NIF_TERM emmap_patomic_and (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 225 | static ERL_NIF_TERM emmap_patomic_or (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 226 | static ERL_NIF_TERM emmap_patomic_xor (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 227 | static ERL_NIF_TERM emmap_patomic_xchg (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 228 | static ERL_NIF_TERM emmap_patomic_cas (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 229 | static ERL_NIF_TERM emmap_patomic_read_int (ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 230 | static ERL_NIF_TERM emmap_patomic_write_int(ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 231 | 232 | // fixed-size blocks storage operations 233 | static ERL_NIF_TERM emmap_init_bs(ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 234 | static ERL_NIF_TERM emmap_read_blk(ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 235 | static ERL_NIF_TERM emmap_store_blk(ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 236 | static ERL_NIF_TERM emmap_free_blk(ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 237 | static ERL_NIF_TERM emmap_read_blocks(ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 238 | static ERL_NIF_TERM emmap_repair_bs(ErlNifEnv*, int argc, const ERL_NIF_TERM argv[]); 239 | 240 | extern "C" { 241 | 242 | static ErlNifFunc nif_funcs[] = 243 | { 244 | {"open_nif", 4, emmap_open}, 245 | {"close_nif", 1, emmap_close}, 246 | {"sync_nif", 1, emmap_sync}, 247 | {"resize_nif", 2, emmap_resize}, 248 | {"pread_nif", 3, emmap_pread}, 249 | {"pwrite_nif", 3, emmap_pwrite}, 250 | {"patomic_read_int_nif", 2, emmap_patomic_read_int}, 251 | {"patomic_write_int_nif", 3, emmap_patomic_write_int}, 252 | {"patomic_add_nif", 3, emmap_patomic_add}, 253 | {"patomic_sub_nif", 3, emmap_patomic_sub}, 254 | {"patomic_and_nif", 3, emmap_patomic_and}, 255 | {"patomic_or_nif", 3, emmap_patomic_or}, 256 | {"patomic_xor_nif", 3, emmap_patomic_xor}, 257 | {"patomic_xchg_nif", 3, emmap_patomic_xchg}, 258 | {"patomic_cas_nif", 4, emmap_patomic_cas}, 259 | {"position_nif", 3, emmap_position}, 260 | {"read_nif", 2, emmap_read}, 261 | {"read_line_nif", 1, emmap_read_line}, 262 | {"init_bs_nif", 2, emmap_init_bs}, 263 | {"read_blk_nif", 2, emmap_read_blk}, 264 | {"store_blk_nif", 2, emmap_store_blk}, 265 | {"free_blk_nif", 2, emmap_free_blk}, 266 | {"read_blocks_nif", 1, emmap_read_blocks}, 267 | {"read_blocks_nif", 3, emmap_read_blocks}, 268 | {"repair_bs_nif", 1, emmap_repair_bs}, 269 | {"repair_bs_nif", 3, emmap_repair_bs}, 270 | }; 271 | 272 | }; 273 | 274 | ERL_NIF_INIT(emmap, nif_funcs, &on_load, nullptr, on_upgrade, nullptr); 275 | 276 | static int on_load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) 277 | { 278 | ErlNifResourceFlags flags = (ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER); 279 | MMAP_RESOURCE = enif_open_resource_type(env, nullptr, "mmap_resource", &emmap_dtor, flags, 0); 280 | 281 | ATOM_ADDRESS = enif_make_atom(env, "address"); 282 | ATOM_ALIGNMENT = enif_make_atom(env, "alignment"); 283 | ATOM_ANON = enif_make_atom(env, "anon"); 284 | ATOM_AUTO_UNLINK = enif_make_atom(env, "auto_unlink"); 285 | ATOM_BOF = enif_make_atom(env, "bof"); 286 | ATOM_CANNOT_CREATE_MAP= enif_make_atom(env, "cannot_create_map"); 287 | ATOM_CHMOD = enif_make_atom(env, "chmod"); 288 | ATOM_CLOSED = enif_make_atom(env, "closed"); 289 | ATOM_CREATE = enif_make_atom(env, "create"); 290 | ATOM_CUR = enif_make_atom(env, "cur"); 291 | ATOM_DEBUG = enif_make_atom(env, "debug"); 292 | ATOM_DIRECT = enif_make_atom(env, "direct"); 293 | ATOM_EACCES = enif_make_atom(env, "eacces"); 294 | ATOM_ENOMEM = enif_make_atom(env, "enomem"); 295 | ATOM_EOF = enif_make_atom(env, "eof"); 296 | ATOM_ERROR = enif_make_atom(env, "error"); 297 | ATOM_EXIST = enif_make_atom(env, "exist"); 298 | ATOM_FALSE = enif_make_atom(env, "false"); 299 | ATOM_FILE = enif_make_atom(env, "file"); 300 | ATOM_FIT = enif_make_atom(env, "fit"); 301 | ATOM_FIXED = enif_make_atom(env, "fixed"); 302 | ATOM_FIXED_SIZE = enif_make_atom(env, "fixed_size"); 303 | ATOM_FULL = enif_make_atom(env, "full"); 304 | ATOM_HUGETLB = enif_make_atom(env, "hugetlb"); 305 | ATOM_HUGE_2MB = enif_make_atom(env, "huge_2mb"); 306 | ATOM_HUGE_1GB = enif_make_atom(env, "huge_1gb"); 307 | ATOM_LOCK = enif_make_atom(env, "lock"); 308 | ATOM_MAX_INC_SIZE = enif_make_atom(env, "max_inc_size"); 309 | ATOM_NOCACHE = enif_make_atom(env, "nocache"); 310 | ATOM_NOLOCK = enif_make_atom(env, "nolock"); 311 | ATOM_NONBLOCK = enif_make_atom(env, "nonblock"); 312 | ATOM_NONE = enif_make_atom(env, "none"); 313 | ATOM_NORESERVE = enif_make_atom(env, "noreserve"); 314 | ATOM_OK = enif_make_atom(env, "ok"); 315 | ATOM_POPULATE = enif_make_atom(env, "populate"); 316 | ATOM_PRIVATE = enif_make_atom(env, "private"); 317 | ATOM_READ = enif_make_atom(env, "read"); 318 | ATOM_SHARED = enif_make_atom(env, "shared"); 319 | #ifndef __APPLE__ 320 | ATOM_SHARED_VALIDATE = enif_make_atom(env, "shared_validate"); 321 | #endif 322 | ATOM_SIZE = enif_make_atom(env, "size"); 323 | #ifndef __APPLE__ 324 | ATOM_SYNC = enif_make_atom(env, "sync"); 325 | #endif 326 | ATOM_TRUE = enif_make_atom(env, "true"); 327 | ATOM_TRUNCATE = enif_make_atom(env, "truncate"); 328 | ATOM_UNINITIALIZED = enif_make_atom(env, "uninitialized"); 329 | ATOM_WRITE = enif_make_atom(env, "write"); 330 | 331 | return 0; 332 | } 333 | 334 | static int on_upgrade(ErlNifEnv* env, void** priv_data, void**, ERL_NIF_TERM load_info) 335 | { 336 | return on_load(env, priv_data, load_info); 337 | } 338 | 339 | static ERL_NIF_TERM make_error_tuple(ErlNifEnv* env, int err) { 340 | return enif_make_tuple2(env, ATOM_ERROR, enif_make_string(env, strerror_compat(err), ERL_NIF_LATIN1)); 341 | } 342 | 343 | static ERL_NIF_TERM make_error_tuple(ErlNifEnv* env, const char* err) { 344 | return enif_make_tuple2(env, ATOM_ERROR, enif_make_string(env, err, ERL_NIF_LATIN1)); 345 | } 346 | 347 | static ERL_NIF_TERM make_error(ErlNifEnv* env, ERL_NIF_TERM err_atom) { 348 | return enif_make_tuple2(env, ATOM_ERROR, err_atom); 349 | } 350 | 351 | static bool decode_flags(ErlNifEnv* env, ERL_NIF_TERM list, int* prot, int* flags, 352 | long* open_flags, long* mode, bool* direct, bool* lock, 353 | bool* auto_unlink, size_t* address, bool* debug, 354 | size_t* max_inc_size, bool* fixed_size, bool* fit) 355 | { 356 | bool l = true; 357 | bool d = false; 358 | int f = MAP_FILE; 359 | int p = PROT_READ; 360 | int arity; 361 | size_t tmp; 362 | const ERL_NIF_TERM* tuple; 363 | 364 | *auto_unlink = false; 365 | *address = 0x0; 366 | *open_flags = O_RDONLY; 367 | *mode = 0600; 368 | *debug = false; 369 | *max_inc_size = 64*1024*1024; 370 | *fixed_size = false; 371 | *fit = false; 372 | *prot = 0; 373 | *flags = 0; 374 | 375 | ERL_NIF_TERM head; 376 | while (enif_get_list_cell(env, list, &head, &list)) { 377 | 378 | if (enif_is_identical(head, ATOM_READ)) { 379 | p |= PROT_READ; 380 | } else if (enif_is_identical(head, ATOM_DEBUG)) { 381 | *debug = true; 382 | } else if (enif_is_identical(head, ATOM_CREATE)) { 383 | *open_flags |= O_CREAT; 384 | } else if (enif_is_identical(head, ATOM_TRUNCATE)) { 385 | *open_flags |= O_TRUNC; 386 | } else if (enif_is_identical(head, ATOM_DIRECT)) { 387 | d = true; 388 | } else if (enif_is_identical(head, ATOM_LOCK)) { 389 | l = true; 390 | } else if (enif_is_identical(head, ATOM_NOLOCK)) { 391 | l = false; 392 | } else if (enif_is_identical(head, ATOM_WRITE)) { 393 | p |= PROT_WRITE; 394 | *open_flags |= O_RDWR; 395 | } else if (enif_is_identical(head, ATOM_PRIVATE)) { 396 | f |= MAP_PRIVATE; 397 | #ifdef MAP_HUGETLB 398 | } else if (enif_is_identical(head, ATOM_HUGETLB)) { 399 | f |= MAP_HUGETLB; 400 | #endif 401 | #ifdef MAP_HUGE_2MB 402 | } else if (enif_is_identical(head, ATOM_HUGE_2MB)) { 403 | f |= MAP_HUGE_2MB; 404 | #endif 405 | #ifdef MAP_HUGE_1GB 406 | } else if (enif_is_identical(head, ATOM_HUGE_1GB)) { 407 | f |= MAP_HUGE_1GB; 408 | #endif 409 | #ifdef MAP_POPULATE 410 | } else if (enif_is_identical(head, ATOM_POPULATE)) { 411 | f |= MAP_POPULATE; 412 | #endif 413 | #ifdef MAP_NONBLOCK 414 | } else if (enif_is_identical(head, ATOM_NONBLOCK)) { 415 | f |= MAP_NONBLOCK; 416 | #endif 417 | #ifndef __APPLE__ 418 | } else if (enif_is_identical(head, ATOM_SHARED_VALIDATE)) { 419 | f |= MAP_SHARED_VALIDATE; 420 | #endif 421 | } else if (enif_is_identical(head, ATOM_SHARED)) { 422 | f |= MAP_SHARED; 423 | } else if (enif_is_identical(head, ATOM_ANON)) { 424 | f |= MAP_ANONYMOUS; 425 | #ifndef __APPLE__ 426 | } else if (enif_is_identical(head, ATOM_SYNC)) { 427 | f |= MAP_SYNC; 428 | #endif 429 | } else if (enif_is_identical(head, ATOM_FIXED)) { 430 | f |= MAP_FIXED; 431 | } else if (enif_is_identical(head, ATOM_FIXED_SIZE)) { 432 | *fixed_size = true; 433 | } else if (enif_is_identical(head, ATOM_FIT)) { 434 | *fit = true; 435 | // } else if (enif_is_identical(head, ATOM_FILE)) { 436 | // f |= MAP_FILE; 437 | } else if (enif_is_identical(head, ATOM_NOCACHE)) { 438 | f |= MAP_NOCACHE; 439 | } else if (enif_is_identical(head, ATOM_UNINITIALIZED)) { 440 | f |= MAP_NOCACHE; 441 | #ifdef MAP_NORESERVE 442 | } else if (enif_is_identical(head, ATOM_NORESERVE)) { 443 | f |= MAP_NORESERVE; 444 | #endif 445 | } else if (enif_is_identical(head, ATOM_AUTO_UNLINK)) { 446 | *auto_unlink = true; 447 | } else if (enif_get_tuple(env, head, &arity, &tuple) && arity == 2) { 448 | if (enif_is_identical (tuple[0], ATOM_ADDRESS) && 449 | enif_get_ulong(env, tuple[1], address)) { 450 | // If address is given, set the "MAP_FIXED" option 451 | if (*address) f |= MAP_FIXED; 452 | continue; 453 | } else if (enif_is_identical (tuple[0], ATOM_CHMOD) && 454 | enif_get_long(env, tuple[1], mode)) { 455 | continue; 456 | } else if (enif_is_identical (tuple[0], ATOM_MAX_INC_SIZE) && 457 | enif_get_ulong(env,tuple[1], &tmp)) { 458 | if (tmp > 0) // Otherwise use default size 459 | *max_inc_size = tmp; 460 | continue; 461 | } else 462 | return false; 463 | } else { 464 | return false; 465 | } 466 | } 467 | 468 | // direct cannot be write 469 | //if (d & ((p & PROT_WRITE) != 0)) 470 | // return 0; 471 | 472 | // default to private 473 | if ((f & (MAP_SHARED|MAP_PRIVATE)) == 0) 474 | f |= MAP_PRIVATE; 475 | 476 | // default to read-only 477 | if ((p & (PROT_READ|PROT_WRITE)) == 0) 478 | p |= PROT_READ; 479 | 480 | *flags = f; 481 | *prot = p; 482 | *direct = d; 483 | *lock = l; 484 | 485 | return true; 486 | } 487 | 488 | static ERL_NIF_TERM emmap_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 489 | { 490 | int flags; 491 | int prot; 492 | long open_flags; 493 | long mode; 494 | bool lock, direct, auto_unlink, dbg, fixed_size, fit; 495 | unsigned long len; 496 | unsigned long offset; 497 | size_t address, max_inc_size; 498 | char path[1024]; 499 | 500 | static_assert(sizeof(long int) == sizeof(size_t), "Architecture not supported"); 501 | 502 | if (argc != 4 503 | || !enif_get_string(env, argv[0], path, sizeof(path), ERL_NIF_LATIN1) 504 | || !enif_get_ulong(env, argv[1], &offset) 505 | || !enif_get_ulong(env, argv[2], &len) 506 | || !decode_flags(env, argv[3], &prot, &flags, &open_flags, &mode, 507 | &direct, &lock, &auto_unlink, 508 | &address, &dbg, &max_inc_size, &fixed_size, &fit)) 509 | return enif_make_badarg(env); 510 | 511 | int fd = -1; 512 | 513 | bool reuse = (open_flags & O_CREAT) == 0; 514 | bool exists = false; 515 | 516 | if ((flags & MAP_ANON) == 0) { 517 | exists = access(path, F_OK) == 0; 518 | 519 | if (!exists) { 520 | if (len == 0) 521 | return make_error_tuple(env, "Missing {size, N} option"); 522 | if (reuse) 523 | return make_error_tuple(env, "Missing 'create' option"); 524 | } 525 | 526 | fd = open(path, open_flags, (mode_t)mode); 527 | 528 | if (fd < 0) { 529 | debug(dbg, "open: %s\r\n", strerror_compat(errno)); 530 | return make_error_tuple(env, errno); 531 | } 532 | 533 | off_t fsize = 0; 534 | struct stat st; 535 | if (fstat(fd, &st) < 0) { 536 | debug(dbg, "fstat: %s\r\n", strerror_compat(errno)); 537 | int err = errno; 538 | close(fd); 539 | return make_error_tuple(env, err); 540 | } else { 541 | fsize = st.st_size; 542 | } 543 | 544 | if (!fit && exists && len > 0 && fsize != long(len) && fsize > 0) { 545 | char buf[1280]; 546 | snprintf(buf, sizeof(buf), "File %s has different size (%ld) than requested (%ld)", 547 | path, long(fsize), len); 548 | close(fd); 549 | return make_error_tuple(env, buf); 550 | } 551 | 552 | bool resized = false, resize_ok = true; 553 | if (exists) { 554 | // Stretch the file size to the requested size 555 | if (fsize == 0 || (fit && fsize < long(len))) { 556 | resized = true; 557 | resize_ok = ftruncate(fd, len) == 0; 558 | } 559 | // Set mapped region length to the greater file size 560 | if (fit && fsize > long(len)) len = fsize; 561 | } 562 | else { 563 | // Truncate, then stretch file 564 | if ((open_flags & (O_CREAT|O_TRUNC)) > 0) { 565 | resized = true; 566 | resize_ok = ftruncate(fd, 0) == 0 && ftruncate(fd, len) == 0; 567 | } 568 | } 569 | 570 | if (resized) { 571 | if (resize_ok) 572 | debug(dbg, "File %s resized to %d bytes\r\n", path, len); 573 | else { 574 | debug(dbg, "ftruncate: %s\r\n", strerror_compat(errno)); 575 | int err = errno; 576 | close(fd); 577 | return make_error_tuple(env, err); 578 | } 579 | } 580 | } 581 | 582 | off_t pa_offset = 0; 583 | if (offset > 0) { 584 | pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1); // offset for mmap() must be page aligned 585 | 586 | if (offset >= len) { 587 | if (fd >= 0) close(fd); 588 | char buf[128]; 589 | snprintf(buf, sizeof(buf), "Offset %ld is past end of file", offset); 590 | return make_error_tuple(env, buf); 591 | } 592 | } 593 | auto size = (size_t)len + offset - pa_offset; 594 | void* res = mmap((void*)address, size, prot, flags, fd, (size_t)pa_offset); 595 | if (res == MAP_FAILED) 596 | return make_error_tuple(env, errno); 597 | 598 | auto handle = (mhandle*)enif_alloc_resource(MMAP_RESOURCE, sizeof(mhandle)); 599 | 600 | if (!handle) { 601 | int err = errno; 602 | munmap(res, size); 603 | char buf[256]; 604 | snprintf(buf, sizeof(buf), "Cannot allocate resource: %s\n", strerror_compat(err)); 605 | return make_error_tuple(env, buf); 606 | } 607 | 608 | new (handle) mhandle(res, path, fd, size, direct, fixed_size, max_inc_size, auto_unlink, prot, lock, dbg); 609 | ERL_NIF_TERM resource = enif_make_resource(env, handle); 610 | enif_release_resource(handle); 611 | 612 | ERL_NIF_TERM keys[] = {ATOM_EXIST, ATOM_SIZE}; 613 | ERL_NIF_TERM vals[] = {exists ? ATOM_TRUE : ATOM_FALSE, enif_make_ulong(env, size)}; 614 | 615 | ERL_NIF_TERM map; 616 | if (!enif_make_map_from_arrays(env, keys, vals, 2, &map)) 617 | return make_error_tuple(env, ATOM_CANNOT_CREATE_MAP); 618 | 619 | debug(handle, "Created memory map %p of size %lu (%s)\r\n", res, size, path); 620 | 621 | return enif_make_tuple3(env, ATOM_OK, resource, map); 622 | } 623 | 624 | static ERL_NIF_TERM emmap_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 625 | { 626 | mhandle* handle; 627 | if (argc!=1 || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle)) 628 | return enif_make_badarg(env); 629 | 630 | RW_LOCK; 631 | bool res = handle->unmap(false); 632 | RW_UNLOCK; 633 | 634 | return res ? ATOM_OK : make_error_tuple(env, errno); 635 | } 636 | 637 | static ERL_NIF_TERM emmap_sync(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 638 | { 639 | mhandle* handle; 640 | if (argc!=1 || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle)) 641 | return enif_make_badarg(env); 642 | 643 | if (!handle->mem) 644 | return ATOM_OK; 645 | 646 | RW_LOCK; 647 | auto res = msync(handle->mem, handle->len, MS_ASYNC); 648 | RW_UNLOCK; 649 | 650 | return res==0 ? ATOM_OK : make_error_tuple(env, strerror_compat(errno)); 651 | } 652 | 653 | static ERL_NIF_TERM emmap_pread(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 654 | { 655 | unsigned long pos, bytes; 656 | mhandle* handle; 657 | if (argc!=3 658 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 659 | || !enif_get_ulong(env, argv[1], &pos) 660 | || !enif_get_ulong(env, argv[2], &bytes) 661 | || pos >= handle->len 662 | ) 663 | return enif_make_badarg(env); 664 | 665 | // Adjust bytes to behave like original file:pread/3 666 | if (pos + bytes > handle->len) bytes = handle->len - pos; 667 | 668 | ErlNifBinary bin; 669 | 670 | if ((handle->prot & PROT_READ) == 0) 671 | return make_error(env, ATOM_EACCES); 672 | 673 | // if this mmap is direct, use a resource binary 674 | if (handle->direct) { 675 | ERL_NIF_TERM res = enif_make_resource_binary 676 | (env, handle, (void*) (((char*)handle->mem) + pos), bytes); 677 | 678 | return enif_make_tuple2(env, ATOM_OK, res); 679 | } 680 | 681 | // When it is non-direct, we have to allocate the binary 682 | if (!enif_alloc_binary((size_t) bytes, &bin)) 683 | return make_error(env, ATOM_ENOMEM); 684 | 685 | R_LOCK; 686 | if (handle->closed()) { 687 | R_UNLOCK; 688 | return make_error(env, ATOM_CLOSED); 689 | } 690 | memcpy(bin.data, (void*) (((char*)handle->mem) + pos), bytes); 691 | R_UNLOCK; 692 | 693 | ERL_NIF_TERM res = enif_make_binary(env, &bin); 694 | return enif_make_tuple2(env, ATOM_OK, res); 695 | } 696 | 697 | #ifndef __APPLE__ 698 | static bool resize_file(mhandle* handle, size_t new_size) { 699 | if (handle->fd >= 0) { 700 | if (ftruncate(handle->fd, new_size) < 0) { 701 | debug(handle->dbg, "ftruncate: %s\r\n", strerror_compat(errno)); 702 | return false; 703 | } 704 | debug(handle->dbg, "File %s resized to %d bytes\r\n", handle->path, new_size); 705 | } 706 | return true; 707 | } 708 | 709 | static int resize(mhandle* handle, unsigned long& new_size) { 710 | if (new_size == 0) 711 | new_size = handle->len < handle->max_inc_size 712 | ? handle->len * 2 : handle->len + handle->max_inc_size; 713 | 714 | if (new_size > handle->len && !resize_file(handle, new_size)) 715 | return -1; 716 | 717 | void* addr = mremap(handle->mem, handle->len, new_size, MREMAP_MAYMOVE); 718 | if (addr == (void*)-1) 719 | return -1; 720 | 721 | if (new_size < handle->len && !resize_file(handle, new_size)) 722 | return -1; 723 | 724 | handle->mem = addr; 725 | handle->len = new_size; 726 | return 0; 727 | } 728 | 729 | static int resize(mhandle* handle) { 730 | unsigned long new_size = 0; 731 | return resize(handle, new_size); 732 | } 733 | #endif 734 | 735 | static ERL_NIF_TERM emmap_resize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 736 | { 737 | unsigned long new_size; 738 | mhandle* handle; 739 | if (argc!=2 740 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 741 | || !enif_get_ulong(env, argv[1], &new_size) 742 | ) 743 | return enif_make_badarg(env); 744 | 745 | if (handle->fixed_size) 746 | return make_error(env, ATOM_FIXED_SIZE); 747 | #ifdef __APPLE__ 748 | return make_error(env, ATOM_FIXED_SIZE); 749 | #else 750 | if ((handle->prot & PROT_WRITE) == 0) { 751 | return make_error(env, ATOM_EACCES); 752 | } 753 | 754 | RW_LOCK; 755 | if (handle->closed()) { 756 | RW_UNLOCK; 757 | return make_error(env, ATOM_CLOSED); 758 | } 759 | 760 | if (resize(handle, new_size) < 0) { 761 | const char* err = strerror_compat(errno); 762 | RW_UNLOCK; 763 | return make_error_tuple(env, err); 764 | } 765 | RW_UNLOCK; 766 | 767 | return enif_make_tuple2(env, ATOM_OK, enif_make_ulong(env, new_size)); 768 | #endif 769 | } 770 | 771 | static ERL_NIF_TERM emmap_pwrite(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 772 | { 773 | ErlNifBinary bin; 774 | unsigned long pos; 775 | mhandle* handle; 776 | if (argc!=3 777 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 778 | || !enif_get_ulong(env, argv[1], &pos) 779 | || !enif_inspect_binary(env, argv[2], &bin) 780 | || (pos + bin.size) > handle->len 781 | ) 782 | return enif_make_badarg(env); 783 | 784 | if ((handle->prot & PROT_WRITE) == 0) { 785 | return make_error(env, ATOM_EACCES); 786 | } 787 | 788 | RW_LOCK; 789 | if (handle->closed()) { 790 | RW_UNLOCK; 791 | return make_error(env, ATOM_CLOSED); 792 | } 793 | 794 | memcpy((void*) (((char*)handle->mem) + pos), bin.data, bin.size); 795 | RW_UNLOCK; 796 | 797 | return ATOM_OK; 798 | } 799 | 800 | /// Atomically read a 64-bit value from the memoty at given position 801 | /// Args: Handle, Position::integer() 802 | /// Return: Value::integer() 803 | static ERL_NIF_TERM emmap_patomic_read_int(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 804 | { 805 | mhandle* handle; 806 | unsigned long pos; 807 | 808 | if (!(argc==2 809 | && enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 810 | && enif_get_ulong (env, argv[1], &pos) 811 | && (pos + 8) <= handle->len 812 | )) 813 | return enif_make_badarg(env); 814 | 815 | if (handle->closed()) 816 | return make_error(env, ATOM_CLOSED); 817 | 818 | 819 | void* mem = (char*)handle->mem + pos; 820 | auto p = static_cast*>(mem); 821 | long res = p->load(std::memory_order_relaxed); 822 | 823 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, res)); 824 | } 825 | 826 | /// Atomically write a 64-bit value to the memoty at given position 827 | /// Args: Handle, Position::integer(), Value::integer() 828 | static ERL_NIF_TERM emmap_patomic_write_int(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 829 | { 830 | mhandle* handle; 831 | unsigned long pos; 832 | long value; 833 | 834 | if (!(argc==3 835 | && enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 836 | && enif_get_ulong (env, argv[1], &pos) 837 | && enif_get_long (env, argv[2], &value) 838 | && (pos + 8) <= handle->len 839 | )) 840 | return enif_make_badarg(env); 841 | 842 | if (handle->closed()) 843 | return make_error(env, ATOM_CLOSED); 844 | 845 | void* mem = (char*)handle->mem + pos; 846 | ((std::atomic*)mem)->store(value); 847 | 848 | return ATOM_OK; 849 | } 850 | 851 | /// Atomically add a 64-bit value to the memoty at given position 852 | /// Args: Handle, Position::integer(), Increment::integer() 853 | /// Return: OldValue::integer() 854 | static ERL_NIF_TERM emmap_patomic_add(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 855 | { 856 | mhandle* handle; 857 | unsigned long pos; 858 | long value; 859 | 860 | if (argc!=3 861 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 862 | || !enif_get_ulong (env, argv[1], &pos) 863 | || !enif_get_long (env, argv[2], &value) 864 | || (pos + 8) > handle->len 865 | ) 866 | return enif_make_badarg(env); 867 | 868 | if ((handle->prot & PROT_WRITE) == 0) 869 | return make_error(env, ATOM_EACCES); 870 | 871 | if (handle->closed()) 872 | return make_error(env, ATOM_CLOSED); 873 | 874 | void* mem = (char*)handle->mem + pos; 875 | long res = ((std::atomic*)mem)->fetch_add(value, std::memory_order_relaxed); 876 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, res)); 877 | } 878 | 879 | /// Atomically subtract a 64-bit value to the memoty at given position 880 | /// Args: Handle, Position::integer(), Decrement::integer() 881 | /// Return: OldValue::integer() 882 | static ERL_NIF_TERM emmap_patomic_sub(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 883 | { 884 | mhandle* handle; 885 | unsigned long pos; 886 | long value; 887 | 888 | if (argc!=3 889 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 890 | || !enif_get_ulong (env, argv[1], &pos) 891 | || !enif_get_long (env, argv[2], &value) 892 | || (pos + 8) > handle->len 893 | ) 894 | return enif_make_badarg(env); 895 | 896 | if ((handle->prot & PROT_WRITE) == 0) 897 | return make_error(env, ATOM_EACCES); 898 | 899 | if (handle->closed()) 900 | return make_error(env, ATOM_CLOSED); 901 | 902 | void* mem = (char*)handle->mem + pos; 903 | long res = ((std::atomic*)mem)->fetch_sub(value, std::memory_order_relaxed); 904 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, res)); 905 | } 906 | 907 | /// Atomically AND a 64-bit value to the memoty at given position 908 | /// Args: Handle, Position::integer(), Value::integer() 909 | /// Return: ResValue::integer() 910 | static ERL_NIF_TERM emmap_patomic_and(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 911 | { 912 | mhandle* handle; 913 | unsigned long pos; 914 | long value; 915 | 916 | if (argc!=3 917 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 918 | || !enif_get_ulong (env, argv[1], &pos) 919 | || !enif_get_long (env, argv[2], &value) 920 | || (pos + 8) > handle->len 921 | ) 922 | return enif_make_badarg(env); 923 | 924 | if ((handle->prot & PROT_WRITE) == 0) 925 | return make_error(env, ATOM_EACCES); 926 | 927 | if (handle->closed()) 928 | return make_error(env, ATOM_CLOSED); 929 | 930 | void* mem = (char*)handle->mem + pos; 931 | long res = ((std::atomic*)mem)->fetch_and(value, std::memory_order_relaxed); 932 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, res)); 933 | } 934 | 935 | /// Atomically OR a 64-bit value to the memoty at given position 936 | /// Args: Handle, Position::integer(), Value::integer() 937 | /// Return: ResValue::integer() 938 | static ERL_NIF_TERM emmap_patomic_or(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 939 | { 940 | mhandle* handle; 941 | unsigned long pos; 942 | long value; 943 | 944 | if (argc!=3 945 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 946 | || !enif_get_ulong (env, argv[1], &pos) 947 | || !enif_get_long (env, argv[2], &value) 948 | || (pos + 8) > handle->len 949 | ) 950 | return enif_make_badarg(env); 951 | 952 | if ((handle->prot & PROT_WRITE) == 0) 953 | return make_error(env, ATOM_EACCES); 954 | 955 | if (handle->closed()) 956 | return make_error(env, ATOM_CLOSED); 957 | 958 | void* mem = (char*)handle->mem + pos; 959 | long res = ((std::atomic*)mem)->fetch_or(value, std::memory_order_relaxed); 960 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, res)); 961 | } 962 | 963 | /// Atomically XOR a 64-bit value to the memoty at given position 964 | /// Args: Handle, Position::integer(), Value::integer() 965 | /// Return: ResValue::integer() 966 | static ERL_NIF_TERM emmap_patomic_xor(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 967 | { 968 | mhandle* handle; 969 | unsigned long pos; 970 | long value; 971 | 972 | if (argc!=3 973 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 974 | || !enif_get_ulong (env, argv[1], &pos) 975 | || !enif_get_long (env, argv[2], &value) 976 | || (pos + 8) > handle->len 977 | ) 978 | return enif_make_badarg(env); 979 | 980 | if ((handle->prot & PROT_WRITE) == 0) 981 | return make_error(env, ATOM_EACCES); 982 | 983 | if (handle->closed()) 984 | return make_error(env, ATOM_CLOSED); 985 | 986 | void* mem = (char*)handle->mem + pos; 987 | long res = ((std::atomic*)mem)->fetch_xor(value, std::memory_order_relaxed); 988 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, res)); 989 | } 990 | 991 | /// Atomically exchange a 64-bit value to the memoty at given position 992 | /// Args: Handle, Position::integer(), Value::integer() 993 | /// Return: ResValue::integer() 994 | static ERL_NIF_TERM emmap_patomic_xchg(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 995 | { 996 | mhandle* handle; 997 | unsigned long pos; 998 | long value; 999 | 1000 | if (argc!=3 1001 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1002 | || !enif_get_ulong (env, argv[1], &pos) 1003 | || !enif_get_long (env, argv[2], &value) 1004 | || (pos + 8) > handle->len 1005 | ) 1006 | return enif_make_badarg(env); 1007 | 1008 | if ((handle->prot & PROT_WRITE) == 0) 1009 | return make_error(env, ATOM_EACCES); 1010 | 1011 | if (handle->closed()) 1012 | return make_error(env, ATOM_CLOSED); 1013 | 1014 | void* mem = (char*)handle->mem + pos; 1015 | long res = ((std::atomic*)mem)->exchange(value, std::memory_order_acq_rel); 1016 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, res)); 1017 | } 1018 | 1019 | /// Atomically compare and swap a 64-bit value to the memoty at given position 1020 | /// with the new_val if the value is equal to the `old_val`. 1021 | /// If the memory at given `pos` is not aligned to long, return `{error, alignment}`. 1022 | /// If CAS fails, return `{false, OldVal}`, where OldVal is the value currently read at `pos`. 1023 | /// Otherwise return `{true, OldVal}`. 1024 | static ERL_NIF_TERM emmap_patomic_cas(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1025 | { 1026 | mhandle* handle; 1027 | unsigned long pos; 1028 | long old_val, new_val; 1029 | 1030 | if (argc!=4 1031 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1032 | || !enif_get_ulong (env, argv[1], &pos) 1033 | || !enif_get_long (env, argv[2], &old_val) 1034 | || !enif_get_long (env, argv[3], &new_val) 1035 | || (pos + 8) > handle->len 1036 | ) 1037 | return enif_make_badarg(env); 1038 | 1039 | if ((handle->prot & PROT_WRITE) == 0) 1040 | return make_error(env, ATOM_EACCES); 1041 | 1042 | if (handle->closed()) 1043 | return make_error(env, ATOM_CLOSED); 1044 | 1045 | void* mem = (char*)handle->mem + pos; 1046 | size_t sz = handle->len - pos; 1047 | 1048 | if (!std::align(sizeof(long), sizeof(long), mem, sz)) 1049 | return enif_make_tuple2(env, ATOM_ERROR, ATOM_ALIGNMENT); 1050 | 1051 | auto p = reinterpret_cast*>(mem); 1052 | auto res = p->compare_exchange_weak(old_val, new_val, std::memory_order_release, 1053 | std::memory_order_relaxed); 1054 | return enif_make_tuple2(env, res ? ATOM_TRUE : ATOM_FALSE, enif_make_long(env, old_val)); 1055 | } 1056 | 1057 | static ERL_NIF_TERM emmap_read(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1058 | { 1059 | mhandle* handle; 1060 | unsigned long bytes; 1061 | 1062 | if (!enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) || 1063 | !enif_get_ulong(env, argv[1], &bytes)) 1064 | return enif_make_badarg(env); 1065 | 1066 | RW_LOCK; 1067 | 1068 | if (handle->position == handle->len) { 1069 | RW_UNLOCK; 1070 | return ATOM_EOF; 1071 | } 1072 | 1073 | unsigned long new_pos = handle->position + bytes; 1074 | if (new_pos > handle->len) { new_pos = handle->len; } 1075 | long size = new_pos - handle->position; 1076 | long start = handle->position; 1077 | handle->position = new_pos; 1078 | RW_UNLOCK; 1079 | 1080 | if (handle->direct) { 1081 | ERL_NIF_TERM res = enif_make_resource_binary 1082 | (env, handle, (void*) (((char*)handle->mem) + start), size); 1083 | 1084 | return enif_make_tuple2(env, ATOM_OK, res); 1085 | 1086 | } else { 1087 | ErlNifBinary bin; 1088 | // When it is non-direct, we have to allocate the binary 1089 | if (!enif_alloc_binary((size_t) size, &bin)) 1090 | return make_error(env, ATOM_ENOMEM); 1091 | 1092 | memcpy(bin.data, (void*) (((char*)handle->mem) + start), size); 1093 | 1094 | ERL_NIF_TERM res = enif_make_binary(env, &bin); 1095 | return enif_make_tuple2(env, ATOM_OK, res); 1096 | } 1097 | } 1098 | 1099 | static ERL_NIF_TERM emmap_read_line(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1100 | { 1101 | mhandle* handle; 1102 | 1103 | if (!enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle)) 1104 | return enif_make_badarg(env); 1105 | 1106 | RW_LOCK; 1107 | 1108 | if (handle->position == handle->len) { 1109 | RW_UNLOCK; 1110 | return ATOM_EOF; 1111 | } 1112 | 1113 | long start = handle->position; 1114 | char* current = ((char*)handle->mem) + handle->position; 1115 | long linelen = 0; // Length of line without trailing \n 1116 | long no_crlf_len = 0; 1117 | bool have_cr = false; 1118 | bool got_eof = false; 1119 | 1120 | // Read buffer until \n or EOF is reached 1121 | while (*current != '\n') { 1122 | handle->position ++; 1123 | current ++; 1124 | if (handle->position == handle->len) { 1125 | got_eof = true; 1126 | break; 1127 | } 1128 | } 1129 | // Step to next byte if EOF is not reached 1130 | if (not got_eof) 1131 | handle->position++; 1132 | 1133 | no_crlf_len = linelen = handle->position - start; 1134 | 1135 | if (not got_eof) { 1136 | // Found LF -- exclude it from line 1137 | no_crlf_len--; 1138 | // If line length before \n is non-zero check if we have \r before \n 1139 | if ((no_crlf_len > 0) && (*(current - 1) == '\r')) { 1140 | have_cr = true; 1141 | no_crlf_len--; 1142 | } 1143 | } 1144 | 1145 | RW_UNLOCK; 1146 | 1147 | // We must not include CR before LF in result, so use direct only without CR 1148 | if ((handle->direct) && (not have_cr)) { 1149 | 1150 | // Include trailing LF if we have it 1151 | ERL_NIF_TERM res = enif_make_resource_binary 1152 | (env, handle, (void*) (((char*)handle->mem) + start), linelen); 1153 | 1154 | return enif_make_tuple2(env, ATOM_OK, res); 1155 | 1156 | } else { 1157 | if (not got_eof) linelen = no_crlf_len + 1; 1158 | 1159 | ErlNifBinary bin; 1160 | // When it is non-direct, we have to allocate the binary 1161 | if (!enif_alloc_binary((size_t) linelen, &bin)) { 1162 | return make_error(env, ATOM_ENOMEM); 1163 | } 1164 | 1165 | memcpy(bin.data, (void*) (((char*)handle->mem) + start), no_crlf_len); 1166 | // Set trailing \n if needed 1167 | if (not got_eof) *(((char*)bin.data) + no_crlf_len) = '\n'; 1168 | 1169 | ERL_NIF_TERM res = enif_make_binary(env, &bin); 1170 | return enif_make_tuple2(env, ATOM_OK, res); 1171 | } 1172 | } 1173 | 1174 | static ERL_NIF_TERM emmap_position(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1175 | { 1176 | mhandle* handle; 1177 | long position; 1178 | long relpos; 1179 | if (argc!=3 1180 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1181 | || !enif_get_long(env, argv[2], &relpos) 1182 | || (argv[1] != ATOM_CUR && argv[1] != ATOM_BOF && argv[1] != ATOM_EOF)) 1183 | return enif_make_badarg(env); 1184 | 1185 | RW_LOCK; 1186 | 1187 | if (argv[1] == ATOM_BOF) 1188 | position = 0L + relpos; 1189 | else if (argv[1] == ATOM_CUR) 1190 | position = handle->position + relpos; 1191 | else if (argv[1] == ATOM_EOF) 1192 | position = handle->len - relpos; 1193 | else 1194 | position = -1; 1195 | 1196 | if (position < 0L || ((unsigned long)position) > handle->len) { 1197 | RW_UNLOCK; 1198 | return enif_make_badarg(env); 1199 | } 1200 | 1201 | handle->position = position; 1202 | RW_UNLOCK; 1203 | 1204 | return enif_make_tuple2(env, ATOM_OK, enif_make_ulong(env, position)); 1205 | } 1206 | 1207 | #ifdef __APPLE__ 1208 | #define RESIZE(env, handle) return make_error(env, ATOM_FIXED_SIZE) 1209 | #else 1210 | #define RESIZE(env, handle) do { \ 1211 | if (handle->fixed_size) \ 1212 | return make_error(env, ATOM_FIXED_SIZE); \ 1213 | if (resize(handle) < 0) { \ 1214 | const char* err = strerror_compat(errno); \ 1215 | return make_error_tuple(env, err); \ 1216 | } \ 1217 | debug(handle, "Resized memory map to %lu bytes\r\n", handle->len); \ 1218 | debug(handle, "New memory range %p ... %p\r\n", handle->mem, (char *)handle->mem + handle->len); \ 1219 | } while (false) 1220 | #endif 1221 | 1222 | // init fixed-size blocks storage 1223 | static ERL_NIF_TERM emmap_init_bs(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1224 | { 1225 | size_t block_size; 1226 | mhandle* handle; 1227 | if (argc!=2 1228 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1229 | || !enif_get_ulong(env, argv[1], &block_size) 1230 | ) 1231 | return enif_make_badarg(env); 1232 | 1233 | if ((handle->prot & PROT_WRITE) == 0) 1234 | return make_error(env, ATOM_EACCES); 1235 | 1236 | rw_lock lock(handle); 1237 | 1238 | if (handle->closed()) 1239 | return make_error(env, ATOM_CLOSED); 1240 | 1241 | while (sizeof(bs_head) > handle->len) RESIZE(env, handle); 1242 | 1243 | bs_head& hdr = *(bs_head*)handle->mem; 1244 | hdr.init(block_size); 1245 | 1246 | return ATOM_OK; 1247 | } 1248 | 1249 | template 1250 | static inline bool read_block(mhandle *handle, int addr, T consumer) { 1251 | bs_head& hdr = *(bs_head*)handle->mem; 1252 | char *mem = (char *)handle->mem; 1253 | void *start = mem + sizeof(bs_head); 1254 | void *stop = mem + handle->len; 1255 | return hdr.read(start, stop, addr, consumer); 1256 | } 1257 | 1258 | static ERL_NIF_TERM emmap_read_blk(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1259 | { 1260 | unsigned long addr; 1261 | mhandle* handle; 1262 | if (argc!=2 1263 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1264 | || !enif_get_ulong(env, argv[1], &addr) 1265 | || sizeof(bs_head) > handle->len 1266 | ) 1267 | return enif_make_badarg(env); 1268 | 1269 | if ((handle->prot & PROT_READ) == 0) 1270 | return make_error(env, ATOM_EACCES); 1271 | 1272 | r_lock lock(handle); 1273 | 1274 | if (handle->closed()) 1275 | return make_error(env, ATOM_CLOSED); 1276 | 1277 | ERL_NIF_TERM res = ATOM_EOF; 1278 | 1279 | // if this mmap is direct, use a resource binary 1280 | if (handle->direct) 1281 | read_block(handle, addr, [env, handle, &res] (void *ptr, size_t len) { 1282 | res = enif_make_resource_binary(env, handle, ptr, len); 1283 | }); 1284 | else 1285 | // When it is non-direct, we have to allocate the binary 1286 | read_block(handle, addr, [env, &res] (void *ptr, size_t len) { 1287 | ErlNifBinary bin; 1288 | if (enif_alloc_binary(len, &bin)) { 1289 | memcpy(bin.data, ptr, len); 1290 | res = enif_make_binary(env, &bin); 1291 | } else 1292 | res = make_error(env, ATOM_ENOMEM); 1293 | }); 1294 | 1295 | return res; 1296 | } 1297 | 1298 | static inline int store_block(mhandle *handle, ErlNifBinary& bin) { 1299 | bs_head& hdr = *(bs_head*)handle->mem; 1300 | char *mem = (char *)handle->mem; 1301 | void *start = mem + sizeof(bs_head); 1302 | void *stop = mem + handle->len; 1303 | return hdr.store(start, stop, bin); 1304 | } 1305 | 1306 | static ERL_NIF_TERM emmap_store_blk(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1307 | { 1308 | mhandle* handle; 1309 | ErlNifBinary bin; 1310 | if (argc!=2 1311 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1312 | || !enif_inspect_binary(env, argv[1], &bin) 1313 | || sizeof(bs_head) > handle->len 1314 | ) 1315 | return enif_make_badarg(env); 1316 | 1317 | if ((handle->prot & PROT_WRITE) == 0) 1318 | return make_error(env, ATOM_EACCES); 1319 | 1320 | rw_lock lock(handle); 1321 | 1322 | if (handle->closed()) 1323 | return make_error(env, ATOM_CLOSED); 1324 | 1325 | if (bin.size != ((bs_head*)handle->mem)->block_size) 1326 | return enif_make_badarg(env); 1327 | 1328 | int addr; 1329 | while ((addr = store_block(handle, bin)) < 0) { 1330 | if (addr < -1) 1331 | RESIZE(env, handle); 1332 | else 1333 | return make_error(env, ATOM_FULL); 1334 | } 1335 | 1336 | return enif_make_ulong(env, addr); 1337 | } 1338 | 1339 | static ERL_NIF_TERM emmap_free_blk(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1340 | { 1341 | unsigned long addr; 1342 | mhandle* handle; 1343 | if (argc!=2 1344 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1345 | || !enif_get_ulong(env, argv[1], &addr) 1346 | || sizeof(bs_head) > handle->len 1347 | ) 1348 | return enif_make_badarg(env); 1349 | 1350 | if ((handle->prot & PROT_WRITE) == 0) 1351 | return make_error(env, ATOM_EACCES); 1352 | 1353 | rw_lock lock(handle); 1354 | 1355 | if (handle->closed()) 1356 | return make_error(env, ATOM_CLOSED); 1357 | 1358 | bs_head& hdr = *(bs_head*)handle->mem; 1359 | char *mem = (char *)handle->mem; 1360 | void *start = mem + sizeof(bs_head); 1361 | void *stop = mem + handle->len; 1362 | return hdr.free(start, stop, addr) ? ATOM_TRUE : ATOM_FALSE; 1363 | } 1364 | 1365 | static ERL_NIF_TERM emmap_read_blocks(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1366 | { 1367 | mhandle* handle; 1368 | if (argc < 1 1369 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1370 | || sizeof(bs_head) > handle->len 1371 | ) 1372 | return enif_make_badarg(env); 1373 | 1374 | unsigned long addr = 0, max = 0; 1375 | 1376 | if (argc > 1) { 1377 | if (argc < 3) return enif_make_badarg(env); 1378 | if (!enif_get_ulong(env, argv[1], &addr)) return enif_make_badarg(env); 1379 | if (!enif_get_ulong(env, argv[2], &max)) return enif_make_badarg(env); 1380 | if (max == 0) return enif_make_badarg(env); 1381 | } 1382 | 1383 | if ((handle->prot & PROT_READ) == 0) 1384 | return make_error(env, ATOM_EACCES); 1385 | 1386 | r_lock lock(handle); 1387 | 1388 | if (handle->closed()) 1389 | return make_error(env, ATOM_CLOSED); 1390 | 1391 | bs_head& hdr = *(bs_head*)handle->mem; 1392 | char *mem = (char *)handle->mem; 1393 | void *start = mem + sizeof(bs_head); 1394 | void *stop = mem + handle->len; 1395 | 1396 | ERL_NIF_TERM res = enif_make_list(env, 0); 1397 | 1398 | int ret; 1399 | if (max > 0) { 1400 | // if this mmap is direct, use a resource binary 1401 | if (handle->direct) 1402 | ret = hdr.fold(start, stop, addr, [env, handle, &max, &res] (int addr, void *ptr, size_t len) -> bool { 1403 | res = enif_make_list_cell(env, 1404 | enif_make_tuple2(env, enif_make_int(env, addr), enif_make_resource_binary(env, handle, ptr, len)), res); 1405 | return --max > 0; 1406 | }); 1407 | else 1408 | // when it is non-direct, we have to allocate the binary 1409 | ret = hdr.fold(start, stop, addr, [env, &max, &res] (int addr, void *ptr, size_t len) -> bool { 1410 | ErlNifBinary bin; 1411 | if (enif_alloc_binary(len, &bin)) { 1412 | memcpy(bin.data, ptr, len); 1413 | res = enif_make_list_cell(env, 1414 | enif_make_tuple2(env, enif_make_int(env, addr), enif_make_binary(env, &bin)), res); 1415 | } else 1416 | res = make_error(env, ATOM_ENOMEM); 1417 | return --max > 0; 1418 | }); 1419 | return ret > 0 ? 1420 | enif_make_tuple2(env, res, enif_make_int(env, ret)) : 1421 | enif_make_tuple2(env, res, ATOM_EOF); 1422 | } 1423 | else { 1424 | // if this mmap is direct, use a resource binary 1425 | if (handle->direct) 1426 | hdr.fold(start, stop, [env, handle, &res] (int addr, void *ptr, size_t len) { 1427 | res = enif_make_list_cell(env, 1428 | enif_make_tuple2(env, enif_make_int(env, addr), enif_make_resource_binary(env, handle, ptr, len)), res); 1429 | }); 1430 | else 1431 | // when it is non-direct, we have to allocate the binary 1432 | hdr.fold(start, stop, [env, &res] (int addr, void *ptr, size_t len) { 1433 | ErlNifBinary bin; 1434 | if (enif_alloc_binary(len, &bin)) { 1435 | memcpy(bin.data, ptr, len); 1436 | res = enif_make_list_cell(env, 1437 | enif_make_tuple2(env, enif_make_int(env, addr), enif_make_binary(env, &bin)), res); 1438 | } else 1439 | res = make_error(env, ATOM_ENOMEM); 1440 | }); 1441 | } 1442 | 1443 | return res; 1444 | } 1445 | 1446 | static ERL_NIF_TERM emmap_repair_bs(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 1447 | { 1448 | mhandle* handle; 1449 | if (argc < 1 1450 | || !enif_get_resource(env, argv[0], MMAP_RESOURCE, (void**)&handle) 1451 | || sizeof(bs_head) > handle->len 1452 | ) 1453 | return enif_make_badarg(env); 1454 | 1455 | unsigned long addr = 0, max = 0; 1456 | 1457 | if (argc > 1) { 1458 | if (argc < 3) return enif_make_badarg(env); 1459 | if (!enif_get_ulong(env, argv[1], &addr)) return enif_make_badarg(env); 1460 | if (!enif_get_ulong(env, argv[2], &max)) return enif_make_badarg(env); 1461 | if (max == 0) return enif_make_badarg(env); 1462 | } 1463 | 1464 | if ((handle->prot & PROT_WRITE) == 0) 1465 | return make_error(env, ATOM_EACCES); 1466 | 1467 | rw_lock lock(handle); 1468 | 1469 | if (handle->closed()) 1470 | return make_error(env, ATOM_CLOSED); 1471 | 1472 | bs_head& hdr = *(bs_head*)handle->mem; 1473 | char *mem = (char *)handle->mem; 1474 | void *start = mem + sizeof(bs_head); 1475 | void *stop = mem + handle->len; 1476 | 1477 | if (max > 0) { 1478 | auto ret = hdr.repair(start, stop, addr, max); 1479 | return ret > 0 ? enif_make_int(env, ret) : ATOM_EOF; 1480 | } 1481 | else { 1482 | hdr.repair(start, stop); 1483 | return ATOM_OK; 1484 | } 1485 | } 1486 | --------------------------------------------------------------------------------