Skip to content

Commit 50fcd34

Browse files
T-y-c-o-o-nNikita Siniachenko
andauthored
[k2] zstd builtins (#1453)
implemented zstd builtins in runtime-light Co-authored-by: Nikita Siniachenko <n.sinyachenko@vk.team>
1 parent 956a725 commit 50fcd34

8 files changed

Lines changed: 223 additions & 14 deletions

File tree

builtin-functions/kphp-light/stdlib/string-functions.txt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,20 +135,16 @@ function unpack ($pattern ::: string, $data ::: string) ::: mixed[] | false;
135135
*/
136136
function base_convert ($number ::: string, $frombase ::: int, $tobase ::: int) ::: string;
137137

138-
// === UNSUPPORTED ===
139-
140-
/** @kphp-extern-func-info stub generation-required */
141-
function str_getcsv($str ::: string, string $delimiter ::: string = ",", string $enclosure ::: string = "\"", string $escape ::: string = "\\") ::: mixed[] | false;
142-
143-
/** @kphp-extern-func-info stub generation-required */
144138
function zstd_compress(string $data, int $level = 3) ::: string | false;
145-
/** @kphp-extern-func-info stub generation-required */
146139
function zstd_uncompress(string $data) ::: string | false;
147-
/** @kphp-extern-func-info stub generation-required */
148140
function zstd_compress_dict(string $data, string $dict) ::: string | false;
149-
/** @kphp-extern-func-info stub generation-required */
150141
function zstd_uncompress_dict(string $data, string $dict) ::: string | false;
151142

143+
// === UNSUPPORTED ===
144+
145+
/** @kphp-extern-func-info stub generation-required */
146+
function str_getcsv($str ::: string, string $delimiter ::: string = ",", string $enclosure ::: string = "\"", string $escape ::: string = "\\") ::: mixed[] | false;
147+
152148
/** @kphp-extern-func-info stub generation-required */
153149
function getimagesize ($name ::: string) ::: mixed;
154150

runtime-light/runtime-light.cmake

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ vk_add_library_pic(runtime-light-pic OBJECT ${RUNTIME_LIGHT_SRC})
5555
target_compile_options(runtime-light-pic PUBLIC ${RUNTIME_LIGHT_COMPILE_FLAGS})
5656
target_link_libraries(runtime-light-pic PUBLIC vk::pic::libc-alloc-wrapper) # it's mandatory to have alloc-wrapper first in the list of link libraries since we
5757
# want to use its symbols in all other libraries
58-
target_link_libraries(runtime-light-pic PUBLIC KPHP_TIMELIB::pic::timelib PCRE2::pic::pcre2 ZLIB::pic::zlib) # third parties
58+
target_link_libraries(runtime-light-pic PUBLIC KPHP_TIMELIB::pic::timelib PCRE2::pic::pcre2 ZLIB::pic::zlib ZSTD::pic::zstd) # third parties
5959

60-
set(RUNTIME_LIGHT_LINK_LIBS "${KPHP_TIMELIB_PIC_LIBRARIES} ${PCRE2_PIC_LIBRARIES} ${ZLIB_PIC_LIBRARIES}")
60+
set(RUNTIME_LIGHT_LINK_LIBS "${KPHP_TIMELIB_PIC_LIBRARIES} ${PCRE2_PIC_LIBRARIES} ${ZLIB_PIC_LIBRARIES} ${ZSTD_PIC_LIBRARIES}")
6161

6262
# =================================================================================================
6363

runtime-light/stdlib/stdlib.cmake

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ prepend(
3939
time/time-functions.cpp
4040
time/time-state.cpp
4141
time/timelib-functions.cpp
42-
zlib/zlib-functions.cpp)
42+
zlib/zlib-functions.cpp
43+
zstd/zstd-functions.cpp)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2025 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#include "runtime-light/stdlib/zstd/zstd-functions.h"
6+
7+
#include <cstddef>
8+
#include <cstdint>
9+
#include <optional>
10+
#include <span>
11+
12+
#define ZSTD_STATIC_LINKING_ONLY
13+
#include "zstd/zstd.h"
14+
15+
#include "common/containers/final_action.h"
16+
#include "runtime-common/core/allocator/script-malloc-interface.h"
17+
#include "runtime-common/core/runtime-core.h"
18+
#include "runtime-common/stdlib/string/string-context.h"
19+
#include "runtime-light/stdlib/diagnostics/logs.h"
20+
21+
namespace {
22+
23+
static_assert(2 * ZSTD_BLOCKSIZE_MAX < StringLibContext::STATIC_BUFFER_LENGTH, "double block size is expected to be less then buffer size");
24+
25+
constexpr ZSTD_customMem zstd_allocator{[](void*, size_t size) noexcept { return kphp::memory::script::alloc(size); },
26+
[](void*, void* ptr) noexcept { return kphp::memory::script::free(ptr); }};
27+
28+
} // namespace
29+
30+
namespace kphp::zstd {
31+
32+
std::optional<string> compress(std::span<const std::byte> data, int64_t level, std::span<const std::byte> dict) noexcept {
33+
const int32_t min_level{ZSTD_minCLevel()};
34+
const int32_t max_level{ZSTD_maxCLevel()};
35+
if (level < min_level || max_level < level) {
36+
kphp::log::warning("zstd_compress: compression level ({}) must be within [{}..{}]", level, min_level, max_level);
37+
return {};
38+
}
39+
40+
ZSTD_CCtx* ctx{ZSTD_createCCtx_advanced(zstd_allocator)};
41+
if (!ctx) {
42+
kphp::log::warning("zstd_compress: can not create context");
43+
return {};
44+
}
45+
const auto finalizer{vk::finally([&ctx]() noexcept { ZSTD_freeCCtx(ctx); })};
46+
47+
size_t result{ZSTD_CCtx_setParameter(ctx, ZSTD_c_compressionLevel, static_cast<int>(level))};
48+
if (ZSTD_isError(result)) {
49+
kphp::log::warning("zstd_compress: can not init context: {}", ZSTD_getErrorName(result));
50+
return {};
51+
}
52+
53+
result = ZSTD_CCtx_loadDictionary_byReference(ctx, dict.data(), dict.size());
54+
if (ZSTD_isError(result)) {
55+
kphp::log::warning("zstd_compress: can not load dict: {}", ZSTD_getErrorName(result));
56+
return {};
57+
}
58+
59+
kphp::log::assertion(ZSTD_CStreamOutSize() <= StringLibContext::STATIC_BUFFER_LENGTH);
60+
ZSTD_outBuffer out{StringLibContext::get().static_buf.get(), StringLibContext::STATIC_BUFFER_LENGTH, 0};
61+
ZSTD_inBuffer in{data.data(), data.size(), 0};
62+
63+
string encoded_string{};
64+
do {
65+
result = ZSTD_compressStream2(ctx, std::addressof(out), std::addressof(in), ZSTD_e_end);
66+
if (ZSTD_isError(result)) {
67+
kphp::log::warning("zstd_compress: got zstd stream compression error: {}", ZSTD_getErrorName(result));
68+
return {};
69+
}
70+
encoded_string.append(static_cast<char*>(out.dst), out.pos);
71+
out.pos = 0;
72+
} while (result);
73+
return encoded_string;
74+
}
75+
76+
std::optional<string> uncompress(std::span<const std::byte> data, std::span<const std::byte> dict) noexcept {
77+
auto size{ZSTD_getFrameContentSize(data.data(), data.size())};
78+
if (size == ZSTD_CONTENTSIZE_ERROR) {
79+
kphp::log::warning("zstd_uncompress: it was not compressed by zstd");
80+
return {};
81+
}
82+
83+
ZSTD_DCtx* ctx{ZSTD_createDCtx_advanced(zstd_allocator)};
84+
if (!ctx) {
85+
kphp::log::warning("zstd_uncompress: can not create context");
86+
return {};
87+
}
88+
const auto finalizer{vk::finally([&ctx]() noexcept { ZSTD_freeDCtx(ctx); })};
89+
90+
size_t result{ZSTD_DCtx_loadDictionary_byReference(ctx, dict.data(), dict.size())};
91+
if (ZSTD_isError(result)) {
92+
kphp::log::warning("zstd_uncompress: can not load dict: {}", ZSTD_getErrorName(result));
93+
return {};
94+
}
95+
96+
if (size != ZSTD_CONTENTSIZE_UNKNOWN) {
97+
if (size > string::max_size()) {
98+
kphp::log::warning("zstd_uncompress: trying to uncompress too large data");
99+
return {};
100+
}
101+
string decompressed{static_cast<string::size_type>(size), false};
102+
result = ZSTD_decompressDCtx(ctx, decompressed.buffer(), size, data.data(), data.size());
103+
if (ZSTD_isError(result)) {
104+
kphp::log::warning("zstd_uncompress: got zstd error: {}", ZSTD_getErrorName(result));
105+
return {};
106+
}
107+
return decompressed;
108+
}
109+
110+
if (ZSTD_isError(result)) {
111+
kphp::log::warning("zstd_uncompress: can not init stream: {}", ZSTD_getErrorName(result));
112+
return {};
113+
}
114+
115+
kphp::log::assertion(ZSTD_DStreamOutSize() <= StringLibContext::STATIC_BUFFER_LENGTH);
116+
ZSTD_inBuffer in{data.data(), data.size(), 0};
117+
ZSTD_outBuffer out{StringLibContext::get().static_buf.get(), StringLibContext::STATIC_BUFFER_LENGTH, 0};
118+
119+
string decoded_string{};
120+
while (in.pos < in.size) {
121+
if (out.pos == out.size) {
122+
decoded_string.append(static_cast<char*>(out.dst), static_cast<string::size_type>(out.pos));
123+
out.pos = 0;
124+
}
125+
126+
result = ZSTD_decompressStream(ctx, std::addressof(out), std::addressof(in));
127+
if (ZSTD_isError(result)) {
128+
kphp::log::warning("zstd_uncompress: can not decompress stream: {}", ZSTD_getErrorName(result));
129+
return {};
130+
}
131+
if (result == 0) {
132+
break;
133+
}
134+
}
135+
decoded_string.append(static_cast<char*>(out.dst), static_cast<string::size_type>(out.pos));
136+
return decoded_string;
137+
}
138+
139+
} // namespace kphp::zstd
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2025 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#pragma once
6+
7+
#include <cstddef>
8+
#include <cstdint>
9+
#include <optional>
10+
#include <span>
11+
12+
#include "runtime-common/core/runtime-core.h"
13+
14+
namespace kphp::zstd {
15+
16+
inline constexpr int64_t DEFAULT_COMPRESS_LEVEL = 3;
17+
18+
std::optional<string> compress(std::span<const std::byte> data, int64_t level = DEFAULT_COMPRESS_LEVEL,
19+
std::span<const std::byte> dict = std::span<const std::byte>{}) noexcept;
20+
21+
std::optional<string> uncompress(std::span<const std::byte> data, std::span<const std::byte> dict = std::span<const std::byte>{}) noexcept;
22+
23+
} // namespace kphp::zstd
24+
25+
inline Optional<string> f$zstd_compress(const string& data, int64_t level = kphp::zstd::DEFAULT_COMPRESS_LEVEL) noexcept {
26+
auto res{kphp::zstd::compress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())}, level)};
27+
if (!res) [[unlikely]] {
28+
return false;
29+
}
30+
return res.value();
31+
}
32+
33+
inline Optional<string> f$zstd_uncompress(const string& data) noexcept {
34+
auto res{kphp::zstd::uncompress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())})};
35+
if (!res) [[unlikely]] {
36+
return false;
37+
}
38+
return res.value();
39+
}
40+
41+
inline Optional<string> f$zstd_compress_dict(const string& data, const string& dict) noexcept {
42+
auto res{kphp::zstd::compress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())}, kphp::zstd::DEFAULT_COMPRESS_LEVEL,
43+
{reinterpret_cast<const std::byte*>(dict.c_str()), static_cast<size_t>(dict.size())})};
44+
if (!res) [[unlikely]] {
45+
return false;
46+
}
47+
return res.value();
48+
}
49+
50+
inline Optional<string> f$zstd_uncompress_dict(const string& data, const string& dict) noexcept {
51+
auto res{kphp::zstd::uncompress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())},
52+
{reinterpret_cast<const std::byte*>(dict.c_str()), static_cast<size_t>(dict.size())})};
53+
if (!res) [[unlikely]] {
54+
return false;
55+
}
56+
return res.value();
57+
}

tests/phpt/zstd/1_compress.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@ok k2_skip
1+
@ok
22
<?php
33

44
function test_compress_levels() {

tests/phpt/zstd/2_uncompress.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@ok k2_skip
1+
@ok
22
<?php
33

44
function test_uncompress() {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@ok
2+
<?php
3+
4+
require_once 'kphp_tester_include.php';
5+
6+
function test_compress_uncompress() {
7+
var_dump(zstd_uncompress((string)zstd_compress("foo bar baz")));
8+
9+
var_dump(zstd_uncompress((string)zstd_compress(str_repeat("foo bar baz", 10000))));
10+
11+
$random_data = (string)openssl_random_pseudo_bytes(1024*1024*15);
12+
assert_true(zstd_uncompress((string)zstd_compress($random_data)) === $random_data);
13+
}
14+
15+
16+
test_compress_uncompress();

0 commit comments

Comments
 (0)