如何安全地确保char*类型将根据OpenGL规范正确实现(在任何平台上)

How can I safely ensure a char* type will be correctly implemented (on any platform) according to OpenGL spec?

本文关键字:范正确 OpenGL 实现 平台 任何 类型 安全 何安全 确保 char      更新时间:2023-10-16

在尝试使用c++和OpenGL3+进行图形编程时,我遇到了一个稍微专业化的理解问题,即字符类型、指向它的指针以及向其他字符指针类型的潜在隐式或显式转换。我想我已经找到了一个解决方案,但我想再问一下你对此的看法。

当前(2014年10月)OpenGL4.5核心配置文件规范(第2.2章"命令语法"中的表2.2)列出了OpenGL数据类型,并明确说明了

GL类型不是C类型。因此,例如,GL类型int在本文档之外被称为GLint,并不一定等同于C类型int。实现必须使用表中指示的位数来表示GL类型。

此表中的GLchar类型被指定为位宽为8的类型,用于表示组成字符串的字符
为了进一步缩小GLchar所提供的范围,我们可以查看GLSL规范(OpenGL着色语言4.50,2014年7月,第3.1章字符集和编译阶段):

OpenGL着色语言使用的源字符集是UTF-8编码方案中的Unicode。

现在,在我想要寻找的任何OpenGL库头中实现这一点的方法都是一个简单的

typedef char GLchar;

这当然与我刚才引用的"GL类型不是C类型"这句话背道而驰。

通常情况下,这不会是一个问题,因为typedef适用于底层类型在未来可能发生变化的情况。

问题始于用户实现。

通过一些关于OpenGL的教程,我发现了各种方法来将GLSL源代码分配给处理它所需的GLchar数组。(请原谅我没有提供所有链接。目前,我还没有这样做所需的声誉。)

网站open.gl喜欢这样做:

const GLchar* vertexSource =
"#version 150 coren"
"in vec2 position;"
"void main() {"
"   gl_Position = vec4(position, 0.0, 1.0);"
"}";

或者这个:

// Shader macro
#define GLSL(src) "#version 150 coren" #src
// Vertex shader
const GLchar* vertexShaderSrc = GLSL(
in vec2 pos;
void main() {
gl_Position = vec4(pos, 0.0, 1.0);
}
);

在lazyfo.net上(第30章加载文本文件着色器),源代码从文件(我喜欢的方法)读取到std::string shaderString变量中,然后用于初始化GL字符串:

const GLchar* shaderSource = shaderString.c_str();

我见过的最冒险的方法是当我在谷歌上搜索加载着色器文件时得到的第一个方法——ClockworkCoders关于加载OpenGL SDK托管的教程,该教程使用显式强制转换——不是GLchar*,而是GLubyte*——如下所示:

GLchar** ShaderSource;
unsigned long len;
ifstream file;
// . . .
len = getFileLength(file);
// . . .
*ShaderSource = (GLubyte*) new char[len+1];

任何像样的c++编译器都会在这里给出一个无效的转换错误。只有在设置了-fpermission标志的情况下,g++编译器才会发出警告。通过这种方式编译,代码将起作用,因为GLubyte最终只是基本类型unsigned chartypedef别名,其长度与char相同。在这种情况下,隐式指针转换可能会生成警告,但仍应执行正确的操作。这违反了C++标准,其中char*signedunsigned char*不兼容,因此这样做是不好的做法。这就引出了我遇到的问题:

我的观点是,所有这些教程都依赖于一个基本事实,即OpenGL规范的实现目前只是基本类型的typedefs形式的窗口装饰。本规范并未涵盖这一假设。更糟糕的是,明确不鼓励将GL类型视为C类型。

如果在未来的任何时候,OpenGL的实现应该改变——无论出于什么原因——使得GLchar不再是char的简单typedef别名,那么像这样的代码将不再编译,因为指向不兼容类型的指针之间没有隐式转换。虽然在某些情况下,可以告诉编译器忽略无效的指针转换,但打开这样糟糕编程的大门可能会导致代码中的各种其他问题。

据我所知,有一个地方做得很好:关于Shader Compilation的官方opengl.org wiki示例,即:

std::string vertexSource = //Get source code for vertex shader.
// . . .
const GLchar *source = (const GLchar *)vertexSource.c_str();

与其他教程的唯一区别是在分配之前将显式转换为const GLchar*。丑陋的是,我知道,然而,就我所见,它使代码安全,不受OpenGL规范未来任何有效实现的影响(总结):UTF-8编码方案中表示字符的一种位大小为8的类型。

为了说明我的推理,我编写了一个简单的类GLchar2,它满足这个规范,但不再允许隐式指针转换到任何基本类型:

// GLchar2.h - a char type of 1 byte length
#include <iostream>
#include <locale> // handle whitespaces
class GLchar2 {
char element; // value of the GLchar2 variable
public:
// default constructor
GLchar2 () {}
// user defined conversion from char to GLchar2
GLchar2 (char element) : element(element) {}
// copy constructor
GLchar2 (const GLchar2& c) : element(c.element) {}
// destructor
~GLchar2 () {}
// assignment operator
GLchar2& operator= (const GLchar2& c) {element = c; return *this;}
// user defined conversion to integral c++ type char
operator char () const {return element;}
};
// overloading the output operator to correctly handle GLchar2
// due to implicit conversion of GLchar2 to char, implementation is unnecessary
//std::ostream& operator<< (std::ostream& o, const GLchar2 character) {
//  char out = character;
//  return o << out;
//}
// overloading the output operator to correctly handle GLchar2*
std::ostream& operator<< (std::ostream& o, const GLchar2* output_string) {
for (const GLchar2* string_it = output_string; *string_it != ''; ++string_it) {
o << *string_it;
}
return o;
}
// overloading the input operator to correctly handle GLchar2
std::istream& operator>> (std::istream& i, GLchar2& input_char) {
char in;
if (i >> in) input_char = in; // this is where the magic happens
return i;
}
// overloading the input operator to correctly handle GLchar2*
std::istream& operator>> (std::istream& i, GLchar2* input_string) {
GLchar2* string_it;
int width = i.width();
std::locale loc;
while (std::isspace((char)i.peek(),loc)) i.ignore(); // ignore leading whitespaces
for (string_it = input_string; (((i.width() == 0 || --width > 0) && !std::isspace((char)i.peek(),loc)) && i >> *string_it); ++string_it);
*string_it = ''; // terminate with null character
i.width(0); // reset width of i
return i;
}

请注意,除了编写类之外,我还实现了输入和输出流运算符的重载,以正确处理从类以及c字符串风格的null终止的GLchar2数组的读取和写入。这在不知道类的内部结构的情况下是可能的,只要它提供类型charGLchar2之间的隐式转换(但不提供它们的指针)。charGLchar2之间或它们的指针类型之间不需要显式转换。

我并不认为GLchar的这个实现是值得的或完整的,但它应该是为了演示。将它与typedef char GLchar1;进行比较,我发现我可以和不能使用这种类型的

// program: test_GLchar.cpp - testing implementation of GLchar
#include <iostream>
#include <fstream>
#include <locale> // handle whitespaces
#include "GLchar2.h"
typedef char GLchar1;
int main () {
// byte size comparison
std::cout << "GLchar1 has a size of " << sizeof(GLchar1) << " byte.n"; // 1
std::cout << "GLchar2 has a size of " << sizeof(GLchar2) << " byte.n"; // 1
// char constructor
const GLchar1 test_char1 = 'o';
const GLchar2 test_char2 = 't';
// default constructor
GLchar2 test_char3;
// char conversion
test_char3 = '3';
// assignment operator
GLchar2 test_char4;
GLchar2 test_char5;
test_char5 = test_char4 = 65; // ASCII value 'A'
// copy constructor
GLchar2 test_char6 = test_char5;
// pointer conversion
const GLchar1* test_string1 = "test string one"; // compiles
//const GLchar1* test_string1 = (const GLchar1*)"test string one"; // compiles
//const GLchar2* test_string2 = "test string two"; // does *not* compile!
const GLchar2* test_string2 = (const GLchar2*)"test string two"; // compiles
std::cout << "A test character of type GLchar1: " << test_char1 << ".n"; // o
std::cout << "A test character of type GLchar2: " << test_char2 << ".n"; // t
std::cout << "A test character of type GLchar2: " << test_char3 << ".n"; // 3
std::cout << "A test character of type GLchar2: " << test_char4 << ".n"; // A
std::cout << "A test character of type GLchar2: " << test_char5 << ".n"; // A
std::cout << "A test character of type GLchar2: " << test_char6 << ".n"; // A
std::cout << "A test string of type GLchar1: " << test_string1 << ".n";
// OUT: A test string of type GLchar1: test string one.n
std::cout << "A test string of type GLchar2: " << test_string2 << ".n";
// OUT: A test string of type GLchar2: test string two.n
// input operator comparison
// test_input_file.vert has the content
//  If you can read this,
//  you can read this.
// (one whitespace before each line to test implementation)
GLchar1* test_string3;
GLchar2* test_string4;
GLchar1* test_string5;
GLchar2* test_string6;
// read character by character
std::ifstream test_file("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string3 = new GLchar1[length+1];
GLchar1* test_it = test_string3;
std::locale loc;
while (test_file >> *test_it) {
++test_it;
while (std::isspace((char)test_file.peek(),loc)) {
*test_it = test_file.peek(); // add whitespaces
test_file.ignore();
++test_it;
}
}
*test_it = '';
std::cout << test_string3 << "n";
// OUT: If you can read this,n you can read this.n
std::cout << length << " " <<test_it - test_string3 << "n";
// OUT: 42 41n
delete[] test_string3;
test_file.close();
}
std::ifstream test_file2("test_input_file.vert");
if (test_file2) {
test_file2.seekg(0, test_file2.end);
int length = test_file2.tellg();
test_file2.seekg(0, test_file2.beg);
test_string4 = new GLchar2[length+1];
GLchar2* test_it = test_string4;
std::locale loc;
while (test_file2 >> *test_it) {
++test_it;
while (std::isspace((char)test_file2.peek(),loc)) {
*test_it = test_file2.peek(); // add whitespaces
test_file2.ignore();
++test_it;
}
}
*test_it = '';
std::cout << test_string4 << "n";
// OUT: If you can read this,n you can read this.n
std::cout << length << " " << test_it - test_string4 << "n";
// OUT: 42 41n
delete[] test_string4;
test_file2.close();
}
// read a word (until delimiter whitespace)
test_file.open("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string5 = new GLchar1[length+1];
//test_file.width(2);
test_file >> test_string5;
std::cout << test_string5 << "n";
// OUT: Ifn
delete[] test_string5;
test_file.close();
}
test_file2.open("test_input_file.vert");
if (test_file2) {
test_file2.seekg(0, test_file2.end);
int length = test_file2.tellg();
test_file2.seekg(0, test_file2.beg);
test_string6 = new GLchar2[length+1];
//test_file2.width(2);
test_file2 >> test_string6;
std::cout << test_string6 << "n";
// OUT: Ifn
delete[] test_string6;
test_file2.close();
}
// read word by word
test_file.open("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string5 = new GLchar1[length+1];
GLchar1* test_it = test_string5;
std::locale loc;
while (test_file >> test_it) {
while (*test_it != '') ++test_it; // test_it points to null character
while (std::isspace((char)test_file.peek(),loc)) {
*test_it = test_file.peek(); // add whitespaces
test_file.ignore();
++test_it;
}
}
std::cout << test_string5 << "n";
// OUT: If you can read this,n you can read this.n
delete[] test_string5;
test_file.close();
}
test_file2.open("test_input_file.vert");
if (test_file2) {
test_file2.seekg(0, test_file2.end);
int length = test_file2.tellg();
test_file2.seekg(0, test_file2.beg);
test_string6 = new GLchar2[length+1];
GLchar2* test_it = test_string6;
std::locale loc;
while (test_file2 >> test_it) {
while (*test_it != '') ++test_it; // test_it points to null character
while (std::isspace((char)test_file2.peek(), loc)) {
*test_it = test_file2.peek(); // add whitespaces
test_file2.ignore();
++test_it;
}
}
std::cout << test_string6 << "n";
// OUT: If you can read this,n you can read this.n
delete[] test_string6;
test_file2.close();
}
// read whole file with std::istream::getline
test_file.open("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string5 = new GLchar1[length+1];
std::locale loc;
while (std::isspace((char)test_file.peek(),loc)) test_file.ignore(); // ignore leading whitespaces
test_file.getline(test_string5, length, '');
std::cout << test_string5  << "n";
// OUT: If you can read this,n you can read this.n
delete[] test_string5;
test_file.close();
}
// no way to do this for a string of GLchar2 as far as I can see
// the getline function that returns c-strings rather than std::string is
// a member of istream and expects to return *this, so overloading is a no go
// however, this works as above:
// read whole file with std::getline
test_file.open("test_input_file.vert");
if (test_file) {
std::locale loc;
while (std::isspace((char)test_file.peek(),loc)) test_file.ignore(); // ignore leading whitespaces
std::string test_stdstring1;
std::getline(test_file, test_stdstring1, '');
test_string5 = (GLchar1*) test_stdstring1.c_str();
std::cout << test_string5 << "n";
// OUT: If you can read this,n you can read this.n
test_file.close();
}
test_file2.open("test_input_file.vert");
if (test_file2) {
std::locale loc;
while (std::isspace((char)test_file2.peek(),loc)) test_file2.ignore(); // ignore leading whitespaces
std::string test_stdstring2;
std::getline(test_file2, test_stdstring2, '');
test_string6 = (GLchar2*) test_stdstring2.c_str();
std::cout << test_string6 << "n";
// OUT: If you can read this,n you can read this.n
test_file.close();
}
return 0;
}

我得出的结论是,至少有两种可行的方法可以编写始终正确处理GLchar字符串而不违反C++标准的代码:

  1. 使用从char数组到GLchar数组的显式转换(不整洁,但可行)。

    const GLchar* sourceCode = (const GLchar*)"some code";

    std::string sourceString = std::string("some code"); // can be from a file GLchar* sourceCode = (GLchar*) sourceString.c_str();

  2. 使用输入流操作符将文件中的字符串直接读取到GLchar数组中。

第二种方法的优点是不需要显式转换,但要实现它,必须动态分配字符串的空间。另一个潜在的缺点是OpenGL不一定会为输入和输出流操作符提供重载来处理它们的类型或指针类型。然而,正如我所展示的,只要至少实现了与char之间的类型转换,那么自己编写这些重载就不是什么魔法。

到目前为止,我还没有发现任何其他可行的重载,用于来自提供与c字符串完全相同语法的文件的输入。

现在,我的问题是:我是否正确地思考了这一点,以便我的代码能够安全地抵御OpenGL可能做出的更改?无论答案是肯定还是否定,是否有更好(即更安全)的方法来确保我的代码的向上兼容性?

此外,我读过这个stackoverflow问答,但据我所知,它不包括字符串,因为它们不是基本类型。

我也不是在问如何编写一个提供隐式指针转换的类(尽管这将是一个有趣的练习)。这个示例类的目的是禁止隐式指针分配,因为如果OpenGL决定更改其实现,则不能保证OpenGL会提供这样的指针分配。

OpenGL规范对语句的含义

"GL类型不是C类型";

是指OpenGL实现可以使用它认为适合此目的的任何类型。这并不意味着该实现被禁止使用C类型。这意味着当使用OpenGL API进行编程时,不必对OpenGL类型的性质进行任何假设。

OpenGL指定GLchar为8位(未明确指定有符号性)。到此为止,不再讨论。因此,只要您以某种方式编写程序,即GLchar被视为8位数据类型,一切都很好。如果你担心有效性,你可以在代码中添加一个静态断言CHAR_BIT == 8,如果平台不遵循这一点,就会抛出错误。

OpenGL标头中的typedef(标头不是标准的BTW)是经过选择的,以便生成的类型符合底层平台ABI的要求。稍微便携一点的gl.h可以进行

#include <stdint.h>
typedef int8_t GLchar;

但这可以归结为int8_t的类型定义,它很可能只是

typedef signed char int8_t;

对于通常的编译器。

如果在未来的任何时候,OpenGL的实现应该改变——无论出于什么原因——使GLchar不再是char的简单typedef别名,那么像这样的代码将不再编译,因为指向不兼容类型的指针之间没有隐式转换

OpenGL不是根据C API或ABI定义的。GLchar是8位,只要API绑定遵守这一点,一切都很好。OpenGL规范为GLchar更改为不同的大小是永远不会发生的,因为这不仅会对现有代码造成严重破坏,还会对GLX等网络协议上的OpenGL造成严重破坏。

更新

注意,如果你关心签名的话。C中有符号性最重要的影响是关于整数提升规则,并且在C中,许多字符操作实际上在ints而不是chars上操作(使用负值作为侧通道),并且对于整数提升规则来说,C中的char类型是有符号的,这并不奇怪。就是这样。

更新2

请注意,您很难找到ABI平台具有CHAR_BIT != 8OpenGL实现的任何C实现——见鬼,我甚至不确定是否存在或曾经存在任何具有CHAR_BIT != 8的C实现。intshort的异常尺寸?当然但是炭?我不知道。

更新3

关于将这整件事放入C++静态类型系统,我建议从std::basic_string派生一个自定义的glstring类,并为GLchar实例化类型、特征和分配器。当谈到大多数ABI中的指针类型兼容性时,GLchar别名为signed char,因此其行为类似于标准的C字符串。

扩展@datenwolf答案:

关于CHAR_BIT:C需要CHAR_BIT >= 8char是C中最小的可寻址单元,OpenGL具有8位类型。这意味着您无法在具有CHAR_BIT != 8的系统上实现一致的OpenGL。。。这与声明一致

。。。不可能在不能满足表2.2中的精确位宽要求的架构上实现GL API。

来自OpenGL 4.5规范

根据GLubyte*char*的转换,AFAIK实际上是完全有效的C和C++。char*被明确允许对所有其他类型进行别名,这就是为什么像这样的代码

int x;
istream &is = ...;
is.read((char*)&x, sizeof(x));

有效。由于sizeof(char) == sizeof(GLchar) == 1是由OpenGL和C的位宽要求组合而成的,因此可以自由访问GLchar的数组作为char的数组。

您引用的带有"GL类型不是C类型"的段落指的是OpenGL规范使用了不带"GL"前缀的"float"answers"int"等类型,因此它表示,尽管使用了这些不固定的名称,但它们(不一定)指的是相应的C类型。相反,在具体的C语言绑定中,名为"int"的OpenGL类型可能是C类型"long"的别名。相反,任何sane绑定都将使用C类型,这样您就可以使用OpenGL类型编写算术表达式(在C中,只能使用内置类型)。

我是否正确考虑了这一点,以便我的代码能够安全地抵御OpenGL可能做出的更改?无论答案是肯定的还是否定的,是否有更好(即更安全)的方法来确保我的代码的向上兼容性?

我认为您从语言律师的角度考虑了太多代码可移植性,而不是专注于学习OpenGL和在实践中编写可移植代码。OpenGL规范没有定义语言绑定,但任何C绑定都不会破坏每个人期望的工作,比如分配const GLchar *str = "hello world"。还请记住,这些是您通常从C++中使用的C绑定,因此标头中不会出现疯狂的类和运算符重载,这实际上限制了实现使用表2.2的基本类型。

编辑:

有一些平台带有CHAR_BIT > 8。请参阅标准委员会关心的异国建筑。尽管今天它主要局限于DSP。POSIX需要CHAR_BIT == 8

永远不要麻烦用标准要求的类型以外的类型实例化basic_stringsiostreams。如果您的类型是其中一个的别名,则可以,但可以直接使用前者。如果你的类型不同,你将进入一场关于特征、地点、代码状态等的无尽噩梦,这些都无法通过便携方式解决。事实上,除了char之外,永远不要使用其他任何东西。