libspirv.cpp: adds c++ api for spvBinaryParse (#5109)

This commit adds a C++ wrapper above the current spvBinaryParse
function. I tried to match it 1:1, except for 2 things:
 - std::function<>& are used. No more function pointers, allowing
   context capture.
 - spv_result_t replaced with a boolean, to match other C++ apis.

Callbacks still return a spv_result_t because the underlying implem
relies on that. The convertion from spv_result_t to boolean is only done
at the boundary.

Signed-off-by: Nathan Gauër <brioche@google.com>
This commit is contained in:
Nathan Gauër 2023-02-14 20:08:20 +01:00 коммит произвёл GitHub
Родитель e150e716ff
Коммит b84c86f718
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 369 добавлений и 0 удалений

Просмотреть файл

@ -402,6 +402,19 @@ typedef struct spv_parsed_instruction_t {
uint16_t num_operands;
} spv_parsed_instruction_t;
typedef struct spv_parsed_header_t {
// The magic number of the SPIR-V module.
uint32_t magic;
// Version number.
uint32_t version;
// Generator's magic number.
uint32_t generator;
// IDs bound for this module (0 < id < bound).
uint32_t bound;
// reserved.
uint32_t reserved;
} spv_parsed_header_t;
typedef struct spv_const_binary_t {
const uint32_t* code;
const size_t wordCount;

Просмотреть файл

@ -31,6 +31,11 @@ using MessageConsumer = std::function<void(
const spv_position_t& /* position */, const char* /* message */
)>;
using HeaderParser = std::function<spv_result_t(
const spv_endianness_t endianess, const spv_parsed_header_t& instruction)>;
using InstructionParser =
std::function<spv_result_t(const spv_parsed_instruction_t& instruction)>;
// C++ RAII wrapper around the C context object spv_context.
class Context {
public:
@ -336,6 +341,23 @@ class SpirvTools {
std::string* text,
uint32_t options = kDefaultDisassembleOption) const;
// Parses a SPIR-V binary, specified as counted sequence of 32-bit words.
// Parsing feedback is provided via two callbacks provided as std::function.
// In a valid parse the parsed-header callback is called once, and
// then the parsed-instruction callback is called once for each instruction
// in the stream.
// Returns true on successful parsing.
// If diagnostic is non-null, a diagnostic is emitted on failed parsing.
// If diagnostic is null the context's message consumer
// will be used to emit any errors. If a callback returns anything other than
// SPV_SUCCESS, then that status code is returned, no further callbacks are
// issued, and no additional diagnostics are emitted.
// This is a wrapper around the C API spvBinaryParse.
bool Parse(const std::vector<uint32_t>& binary,
const HeaderParser& header_parser,
const InstructionParser& instruction_parser,
spv_diagnostic* diagnostic = nullptr);
// Validates the given SPIR-V |binary|. Returns true if no issues are found.
// Otherwise, returns false and communicates issues via the message consumer
// registered.

Просмотреть файл

@ -108,6 +108,40 @@ bool SpirvTools::Disassemble(const uint32_t* binary, const size_t binary_size,
return status == SPV_SUCCESS;
}
struct CxxParserContext {
const HeaderParser& header_parser;
const InstructionParser& instruction_parser;
};
bool SpirvTools::Parse(const std::vector<uint32_t>& binary,
const HeaderParser& header_parser,
const InstructionParser& instruction_parser,
spv_diagnostic* diagnostic) {
CxxParserContext parser_context = {header_parser, instruction_parser};
spv_parsed_header_fn_t header_fn_wrapper =
[](void* user_data, spv_endianness_t endianness, uint32_t magic,
uint32_t version, uint32_t generator, uint32_t id_bound,
uint32_t reserved) {
CxxParserContext* ctx = reinterpret_cast<CxxParserContext*>(user_data);
spv_parsed_header_t header = {magic, version, generator, id_bound,
reserved};
return ctx->header_parser(endianness, header);
};
spv_parsed_instruction_fn_t instruction_fn_wrapper =
[](void* user_data, const spv_parsed_instruction_t* instruction) {
CxxParserContext* ctx = reinterpret_cast<CxxParserContext*>(user_data);
return ctx->instruction_parser(*instruction);
};
spv_result_t status = spvBinaryParse(
impl_->context, &parser_context, binary.data(), binary.size(),
header_fn_wrapper, instruction_fn_wrapper, diagnostic);
return status == SPV_SUCCESS;
}
bool SpirvTools::Validate(const std::vector<uint32_t>& binary) const {
return Validate(binary.data(), binary.size());
}

Просмотреть файл

@ -214,6 +214,40 @@ class BinaryParseTest : public spvtest::TextToBinaryTestBase<::testing::Test> {
MockParseClient client_;
};
class CxxBinaryParseTest
: public spvtest::TextToBinaryTestBase<::testing::Test> {
protected:
CxxBinaryParseTest() {
header_parser_ = [this](const spv_endianness_t endianness,
const spv_parsed_header_t& header) {
return this->client_.Header(endianness, header.magic, header.version,
header.generator, header.bound,
header.reserved);
};
instruction_parser_ = [this](const spv_parsed_instruction_t& instruction) {
return this->client_.Instruction(ParsedInstruction(instruction));
};
}
~CxxBinaryParseTest() override { spvDiagnosticDestroy(diagnostic_); }
void Parse(const SpirvVector& words, bool expected_result,
bool flip_words = false,
spv_target_env env = SPV_ENV_UNIVERSAL_1_0) {
SpirvVector flipped_words(words);
MaybeFlipWords(flip_words, flipped_words.begin(), flipped_words.end());
spvtools::SpirvTools tools(env);
EXPECT_EQ(expected_result, tools.Parse(flipped_words, header_parser_,
instruction_parser_, &diagnostic_));
}
spv_diagnostic diagnostic_ = nullptr;
MockParseClient client_;
HeaderParser header_parser_;
InstructionParser instruction_parser_;
};
// Adds an EXPECT_CALL to client_->Header() with appropriate parameters,
// including bound. Returns the EXPECT_CALL result.
#define EXPECT_HEADER(bound) \
@ -235,6 +269,16 @@ TEST_F(BinaryParseTest, EmptyModuleHasValidHeaderAndNoInstructionCallbacks) {
}
}
TEST_F(CxxBinaryParseTest, EmptyModuleHasValidHeaderAndNoInstructionCallbacks) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully("");
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
Parse(words, true, endian_swap);
EXPECT_EQ(nullptr, diagnostic_);
}
}
TEST_F(BinaryParseTest, NullDiagnosticsIsOkForGoodParse) {
const auto words = CompileSuccessfully("");
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
@ -245,6 +289,15 @@ TEST_F(BinaryParseTest, NullDiagnosticsIsOkForGoodParse) {
words.size(), invoke_header, invoke_instruction, nullptr));
}
TEST_F(CxxBinaryParseTest, NullDiagnosticsIsOkForGoodParse) {
const auto words = CompileSuccessfully("");
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
EXPECT_EQ(true,
tools.Parse(words, header_parser_, instruction_parser_, nullptr));
}
TEST_F(BinaryParseTest, NullDiagnosticsIsOkForBadParse) {
auto words = CompileSuccessfully("");
words.push_back(0xffffffff); // Certainly invalid instruction header.
@ -256,6 +309,16 @@ TEST_F(BinaryParseTest, NullDiagnosticsIsOkForBadParse) {
words.size(), invoke_header, invoke_instruction, nullptr));
}
TEST_F(CxxBinaryParseTest, NullDiagnosticsIsOkForBadParse) {
auto words = CompileSuccessfully("");
words.push_back(0xffffffff); // Certainly invalid instruction header.
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
EXPECT_EQ(false,
tools.Parse(words, header_parser_, instruction_parser_, nullptr));
}
// Make sure that we don't blow up when both the consumer and the diagnostic are
// null.
TEST_F(BinaryParseTest, NullConsumerNullDiagnosticsForBadParse) {
@ -272,6 +335,18 @@ TEST_F(BinaryParseTest, NullConsumerNullDiagnosticsForBadParse) {
invoke_header, invoke_instruction, nullptr));
}
TEST_F(CxxBinaryParseTest, NullConsumerNullDiagnosticsForBadParse) {
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_1);
tools.SetMessageConsumer(nullptr);
auto words = CompileSuccessfully("");
words.push_back(0xffffffff); // Certainly invalid instruction header.
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
EXPECT_EQ(false,
tools.Parse(words, header_parser_, instruction_parser_, nullptr));
}
TEST_F(BinaryParseTest, SpecifyConsumerNullDiagnosticsForGoodParse) {
const auto words = CompileSuccessfully("");
@ -289,6 +364,21 @@ TEST_F(BinaryParseTest, SpecifyConsumerNullDiagnosticsForGoodParse) {
EXPECT_EQ(0, invocation);
}
TEST_F(CxxBinaryParseTest, SpecifyConsumerNullDiagnosticsForGoodParse) {
const auto words = CompileSuccessfully("");
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_1);
int invocation = 0;
tools.SetMessageConsumer([&invocation](spv_message_level_t, const char*,
const spv_position_t&,
const char*) { ++invocation; });
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
EXPECT_EQ(true,
tools.Parse(words, header_parser_, instruction_parser_, nullptr));
EXPECT_EQ(0, invocation);
}
TEST_F(BinaryParseTest, SpecifyConsumerNullDiagnosticsForBadParse) {
auto words = CompileSuccessfully("");
@ -315,6 +405,30 @@ TEST_F(BinaryParseTest, SpecifyConsumerNullDiagnosticsForBadParse) {
EXPECT_EQ(1, invocation);
}
TEST_F(CxxBinaryParseTest, SpecifyConsumerNullDiagnosticsForBadParse) {
auto words = CompileSuccessfully("");
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_1);
int invocation = 0;
tools.SetMessageConsumer(
[&invocation](spv_message_level_t level, const char* source,
const spv_position_t& position, const char* message) {
++invocation;
EXPECT_EQ(SPV_MSG_ERROR, level);
EXPECT_STREQ("input", source);
EXPECT_EQ(0u, position.line);
EXPECT_EQ(0u, position.column);
EXPECT_EQ(1u, position.index);
EXPECT_STREQ("Invalid opcode: 65535", message);
});
words.push_back(0xffffffff); // Certainly invalid instruction header.
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
EXPECT_EQ(false,
tools.Parse(words, header_parser_, instruction_parser_, nullptr));
EXPECT_EQ(1, invocation);
}
TEST_F(BinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForGoodParse) {
const auto words = CompileSuccessfully("");
@ -333,6 +447,22 @@ TEST_F(BinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForGoodParse) {
EXPECT_EQ(nullptr, diagnostic_);
}
TEST_F(CxxBinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForGoodParse) {
const auto words = CompileSuccessfully("");
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_1);
int invocation = 0;
tools.SetMessageConsumer([&invocation](spv_message_level_t, const char*,
const spv_position_t&,
const char*) { ++invocation; });
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
EXPECT_EQ(true, tools.Parse(words, header_parser_, instruction_parser_,
&diagnostic_));
EXPECT_EQ(0, invocation);
EXPECT_EQ(nullptr, diagnostic_);
}
TEST_F(BinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForBadParse) {
auto words = CompileSuccessfully("");
@ -352,6 +482,23 @@ TEST_F(BinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForBadParse) {
EXPECT_STREQ("Invalid opcode: 65535", diagnostic_->error);
}
TEST_F(CxxBinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForBadParse) {
auto words = CompileSuccessfully("");
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_1);
int invocation = 0;
tools.SetMessageConsumer([&invocation](spv_message_level_t, const char*,
const spv_position_t&,
const char*) { ++invocation; });
words.push_back(0xffffffff); // Certainly invalid instruction header.
EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback.
EXPECT_EQ(false, tools.Parse(words, header_parser_, instruction_parser_,
&diagnostic_));
EXPECT_EQ(0, invocation);
EXPECT_STREQ("Invalid opcode: 65535", diagnostic_->error);
}
TEST_F(BinaryParseTest,
ModuleWithSingleInstructionHasValidHeaderAndInstructionCallback) {
for (bool endian_swap : kSwapEndians) {
@ -365,6 +512,19 @@ TEST_F(BinaryParseTest,
}
}
TEST_F(CxxBinaryParseTest,
ModuleWithSingleInstructionHasValidHeaderAndInstructionCallback) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully("%1 = OpTypeVoid");
InSequence calls_expected_in_specific_order;
EXPECT_HEADER(2).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1)))
.WillOnce(Return(SPV_SUCCESS));
Parse(words, true, endian_swap);
EXPECT_EQ(nullptr, diagnostic_);
}
}
TEST_F(BinaryParseTest, NullHeaderCallbackIsIgnored) {
const auto words = CompileSuccessfully("%1 = OpTypeVoid");
EXPECT_CALL(client_, Header(_, _, _, _, _, _))
@ -408,6 +568,22 @@ TEST_F(BinaryParseTest, TwoScalarTypesGenerateTwoInstructionCallbacks) {
}
}
TEST_F(CxxBinaryParseTest, TwoScalarTypesGenerateTwoInstructionCallbacks) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
"%1 = OpTypeVoid "
"%2 = OpTypeInt 32 1");
InSequence calls_expected_in_specific_order;
EXPECT_HEADER(3).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1)))
.WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(MakeParsedInt32TypeInstruction(2)))
.WillOnce(Return(SPV_SUCCESS));
Parse(words, true, endian_swap);
EXPECT_EQ(nullptr, diagnostic_);
}
}
TEST_F(BinaryParseTest, EarlyReturnWithZeroPassingCallbacks) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
@ -423,6 +599,21 @@ TEST_F(BinaryParseTest, EarlyReturnWithZeroPassingCallbacks) {
}
}
TEST_F(CxxBinaryParseTest, EarlyReturnWithZeroPassingCallbacks) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
"%1 = OpTypeVoid "
"%2 = OpTypeInt 32 1");
InSequence calls_expected_in_specific_order;
EXPECT_HEADER(3).WillOnce(Return(SPV_ERROR_INVALID_BINARY));
// Early exit means no calls to Instruction().
EXPECT_CALL(client_, Instruction(_)).Times(0);
Parse(words, false, endian_swap);
// On error, the binary parser doesn't generate its own diagnostics.
EXPECT_EQ(nullptr, diagnostic_);
}
}
TEST_F(BinaryParseTest,
EarlyReturnWithZeroPassingCallbacksAndSpecifiedResultCode) {
for (bool endian_swap : kSwapEndians) {
@ -440,6 +631,23 @@ TEST_F(BinaryParseTest,
}
}
TEST_F(CxxBinaryParseTest,
EarlyReturnWithZeroPassingCallbacksAndSpecifiedResultCode) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
"%1 = OpTypeVoid "
"%2 = OpTypeInt 32 1");
InSequence calls_expected_in_specific_order;
EXPECT_HEADER(3).WillOnce(Return(SPV_REQUESTED_TERMINATION));
// Early exit means no calls to Instruction().
EXPECT_CALL(client_, Instruction(_)).Times(0);
Parse(words, false, endian_swap);
// On early termination, the binary parser doesn't generate its own
// diagnostics.
EXPECT_EQ(nullptr, diagnostic_);
}
}
TEST_F(BinaryParseTest, EarlyReturnWithOnePassingCallback) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
@ -457,6 +665,23 @@ TEST_F(BinaryParseTest, EarlyReturnWithOnePassingCallback) {
}
}
TEST_F(CxxBinaryParseTest, EarlyReturnWithOnePassingCallback) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
"%1 = OpTypeVoid "
"%2 = OpTypeInt 32 1 "
"%3 = OpTypeFloat 32");
InSequence calls_expected_in_specific_order;
EXPECT_HEADER(4).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1)))
.WillOnce(Return(SPV_REQUESTED_TERMINATION));
Parse(words, false, endian_swap);
// On early termination, the binary parser doesn't generate its own
// diagnostics.
EXPECT_EQ(nullptr, diagnostic_);
}
}
TEST_F(BinaryParseTest, EarlyReturnWithTwoPassingCallbacks) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
@ -476,6 +701,25 @@ TEST_F(BinaryParseTest, EarlyReturnWithTwoPassingCallbacks) {
}
}
TEST_F(CxxBinaryParseTest, EarlyReturnWithTwoPassingCallbacks) {
for (bool endian_swap : kSwapEndians) {
const auto words = CompileSuccessfully(
"%1 = OpTypeVoid "
"%2 = OpTypeInt 32 1 "
"%3 = OpTypeFloat 32");
InSequence calls_expected_in_specific_order;
EXPECT_HEADER(4).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1)))
.WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(MakeParsedInt32TypeInstruction(2)))
.WillOnce(Return(SPV_REQUESTED_TERMINATION));
Parse(words, false, endian_swap);
// On early termination, the binary parser doesn't generate its own
// diagnostics.
EXPECT_EQ(nullptr, diagnostic_);
}
}
TEST_F(BinaryParseTest, InstructionWithStringOperand) {
for (bool endian_swap : kSwapEndians) {
const std::string str =
@ -501,6 +745,31 @@ TEST_F(BinaryParseTest, InstructionWithStringOperand) {
}
}
TEST_F(CxxBinaryParseTest, InstructionWithStringOperand) {
for (bool endian_swap : kSwapEndians) {
const std::string str =
"the future is already here, it's just not evenly distributed";
const auto str_words = MakeVector(str);
const auto instruction = MakeInstruction(spv::Op::OpName, {99}, str_words);
const auto words = Concatenate({ExpectedHeaderForBound(100), instruction});
InSequence calls_expected_in_specific_order;
EXPECT_HEADER(100).WillOnce(Return(SPV_SUCCESS));
const auto operands = std::vector<spv_parsed_operand_t>{
MakeSimpleOperand(1, SPV_OPERAND_TYPE_ID),
MakeLiteralStringOperand(2, static_cast<uint16_t>(str_words.size()))};
EXPECT_CALL(
client_,
Instruction(ParsedInstruction(spv_parsed_instruction_t{
instruction.data(), static_cast<uint16_t>(instruction.size()),
uint16_t(spv::Op::OpName), SPV_EXT_INST_TYPE_NONE, 0 /*type id*/,
0 /* No result id for OpName*/, operands.data(),
static_cast<uint16_t>(operands.size())})))
.WillOnce(Return(SPV_SUCCESS));
Parse(words, true, endian_swap);
EXPECT_EQ(nullptr, diagnostic_);
}
}
// Checks for non-zero values for the result_id and ext_inst_type members
// spv_parsed_instruction_t.
TEST_F(BinaryParseTest, ExtendedInstruction) {
@ -534,6 +803,37 @@ TEST_F(BinaryParseTest, ExtendedInstruction) {
EXPECT_EQ(nullptr, diagnostic_);
}
TEST_F(CxxBinaryParseTest, ExtendedInstruction) {
const auto words = CompileSuccessfully(
"%extcl = OpExtInstImport \"OpenCL.std\" "
"%result = OpExtInst %float %extcl sqrt %x");
EXPECT_HEADER(5).WillOnce(Return(SPV_SUCCESS));
EXPECT_CALL(client_, Instruction(_)).WillOnce(Return(SPV_SUCCESS));
// We're only interested in the second call to Instruction():
const auto operands = std::vector<spv_parsed_operand_t>{
MakeSimpleOperand(1, SPV_OPERAND_TYPE_TYPE_ID),
MakeSimpleOperand(2, SPV_OPERAND_TYPE_RESULT_ID),
MakeSimpleOperand(3,
SPV_OPERAND_TYPE_ID), // Extended instruction set Id
MakeSimpleOperand(4, SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER),
MakeSimpleOperand(5, SPV_OPERAND_TYPE_ID), // Id of the argument
};
const auto instruction = MakeInstruction(
spv::Op::OpExtInst,
{2, 3, 1, static_cast<uint32_t>(OpenCLLIB::Entrypoints::Sqrt), 4});
EXPECT_CALL(client_,
Instruction(ParsedInstruction(spv_parsed_instruction_t{
instruction.data(), static_cast<uint16_t>(instruction.size()),
uint16_t(spv::Op::OpExtInst), SPV_EXT_INST_TYPE_OPENCL_STD,
2 /*type id*/, 3 /*result id*/, operands.data(),
static_cast<uint16_t>(operands.size())})))
.WillOnce(Return(SPV_SUCCESS));
// Since we are actually checking the output, don't test the
// endian-swapped version.
Parse(words, true, false);
EXPECT_EQ(nullptr, diagnostic_);
}
// A binary parser diagnostic test case where we provide the words array
// pointer and word count explicitly.
struct WordsAndCountDiagnosticCase {